I have a component with a large number of props, but I only need to test a handful are being called in my enzyme tests.
Is there a way to dynamically pass in all the props with stubs?
const film = true;
const ArabianNightsComponent = (Aladdin, AliBaba, ThiefOne, ThiefTwo, ..... ThiefForty) => {
// do something with props
const hero = film? Aladdin(): AliBaba();
return (<div>{hero}</div>);
}
If I want to test this with enzye (say, using shallow), how do I dynamically pass in TheifOne to ThiefForty?
I've tried this:
let wrapper = shallow(<ArabianNights Aladdin={sinon.stub()} AliBaba={sinon.stub()}/>);
But I get a load of errors from PropTypes that I'm missing required props, and as well as this I get errors from lower down components, when I run wrapper.html() because it's missing their props.
You can use the PropTypes to figure out what props a component has, and map the values to the stubs you want to pass in.
let Aladdin, AliBaba;
let wrapper;
beforeEach( () => {
Aladdin = sinon.stub();
AliBaba = sinon.stub();
const stubsToTest = {Aladdin, AliBaba}; // equiv. {"Aladdin": Sinon.stub() ...}
// uses lodash to map values of oject
const otherStubs = _.mapValues(ArabianNights.propTypes, () => sinon.stub());
// now looks like {ThiefOne: stub, ThiefTwo: stub, ...}
const props = {...otherStubs, ...stubToTest}; // combine objects
wrapper = shallow(<ArabianNights {...props} />
});
Here, the stubsToTest comes after the otherStubs so that they aren't overwritten.
Then continue with tests as expected.
Related
I am trying to find an item from a collection, from the code below, in order to update my react component, the propertState object isnt empty, it contains a list which i have console logged, however I seem to get an underfined object when i console log the value returned from my findProperty function... I am trying update my localState with that value so that my component can render the right data.
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState()
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
return (<>{property.propertyName}</>)
}
export default PropertyComponent
There are a few things that you shall consider when you are finding data and updating states based on external sources of data --useParams--
I will try to explain the solution by dividing your code in small pieces
const PropertyComponent = () => {
const { propertyId } = useParams();
Piece A: Consider that useParams is a hook connected to the router, that means that you component might be reactive and will change every time that a param changes in the URL. Your param might be undefined or an string depending if the param is present in your URL
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
Piece B: useSelector is other property that will make your component reactive to changes related to that selector. Your selector might return undefined or something based on your selection logic.
const[property, setProperty] = useState()
Piece C: Your state that starts as undefined in the first render.
So far we have just discovered 3 pieces of code that might start as undefined or not.
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
Piece D: Here is where more problems start appearing, you are telling your code that in every render a function findProperty will be created and inside of it you are calling the setter of your state --setProperty--, generating an internal dependency.
I would suggest to think about the actions that you want to do in simple steps and then you can understand where each piece of code belongs to where.
Let's subdivide this last piece of code --Piece D-- but in steps, you want to:
Find something.
The find should happen if you have an array where to find and a property.
With the result I want to notify my component that something was found.
Step 1 and 2 can happen in a function defined outside of your component:
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
NOTE: I took the liberty of modify your code by simplifying a little
bit your find function.
Now we need to do the most important step, make your component react at the right time
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState({ propertyName: '' }); // I suggest to add default values to have more predictable returns in your component
/**
* Here is where the magic begins and we try to mix all of our values in a consistent way (thinking on the previous pieces and the potential "undefined" values) We need to tell react "do something when the data is ready", for that reason we will use an effect
*/
useEffect(() => {
// This effect will run every time that the dependencies --second argument-- changes, then you react afterwards.
if(propertyId, propertyState.properties) {
const propertyFound = findProperty(propertyId, propertyState.properties);
if(propertyFound){ // Only if we have a result we will update our state.
setProperty(propertyFound);
}
}
}, [propertyId, propertyState.properties])
return (<>{property.propertyName}</>)
}
export default PropertyComponent
I think that in this way your intention might be more direct, but for sure there are other ways to do this. Depending of your intentions your code should be different, for instance I have a question:
What is it the purpose of this component? If its just for getting the property you could do a derived state, a little bit more complex selector. E.G.
function propertySelectorById(id) {
return function(store) {
const allProperties = propertiesStateSelector(store);
const foundProperty = findProperty(id, allProperties);
if( foundProperty ) {
return foundProperty;
} else {
return null; // Or empty object, up to you
}
}
}
Then you can use it in any component that uses the useParam, or just create a simple hook. E.G.
function usePropertySelectorHook() {
const { propertyId } = useParams();
const property = useSelector(propertySelectorById(propertyId));
return property;
}
And afterwards you can use this in any component
functon AnyComponent() {
const property = usePropertySelectorHook();
return <div> Magic {property}</div>
}
NOTE: I didn't test all the code, I wrote it directly in the comment but I think that should work.
Like this I think that there are even more ways to solve this, but its enough for now, hope that this helped you.
do you try this:
const found = propertyState.properties.find(element => element.propertyId === propertyId);
setProperty(found);
instead of all function findProperty
I am using an array of components that are interested depending on various conditions i.e the order and number of elements in the array are dynamic as shown below:
useEffect(() => {
const comp = [];
// if(condition1===true){
comp.push(<MyComp onChange={onValueChange} />);
// }
// if(condition2===true){
comp.push(<YourComp onChange={onValueChange} />);
// }
// if(condition3===true){
comp.push(<HisComp onChange={onValueChange} />);
// }
setComponents(comp);
}, []);
To each of the components in the array, there could be some sort of input control like input-text, input-number, text-area, chips, radio, checkbox, etc.
So there is an onChange event linked to each of these components.
I am using a common onValueChange function which is passed as a callback to these components. In the onValueChange I need 2 things:
changed value (from child component)
activeIndex (from same component)
const onValueChange = (val) => {
console.log("onChange Valled", val, "activeIndex==", activeIndex);
};
But here I am not able to fetch the updated value on activeIndex, it always gives zero no matter in what active step I am in.
Sandbox DEMO
useEffect(() => {
setComponents((previousValues)=>{
// if you want to add on previous state
const comp = [...previousValues];
// if you want to overwrite previous state
const comp = [];
if(condition1===true){
comp.push();
}
if(condition2===true){
comp.push();
}
if(condition3===true){
comp.push();
}
return comp;
});
}, []);
Try using useCallback with dependency array. Also try to avoid storing components in state - the office advice - what shouldn’t go in state?
const onValueChange = useCallback((val) => {
console.log("onChange Valled", val, "activeIndex==", activeIndex);
},[activeIndex];
For rendering try something like below.
condition1===true && <MyComp onChange={onValueChange} />
or create a function which returns the component eg: getComponent(condition) and use this in render/return. Make sure you wrap getComponent in useCallback with empty dependency array []
I am trying to convert this demo into a function component. I am following these steps and I am stuck with the following:
Class version:
this.appointmentForm = connectProps(AppointmentFormContainer, () => {
const {
editingFormVisible,
editingAppointment,
data,
addedAppointment,
isNewAppointment,
previousAppointment,
} = this.state;
Function conversion attempt:
const [appointmentForm, setappointmentForm] = useState({});
setappointmentForm(connectProps(AppointmentFormContainer, () => {
const {
editingFormVisible,
editingAppointment,
data,
addedAppointment,
isNewAppointment,
previousAppointment,
};
The error with this version (tried several) is : "Parsing error: 'Const declarations' require an initialization value." it refers to the const in the line under the setappointmentForm but getting rid of it is incorrect as well. If the whole code is needed I will put it but it is quite long. Any ideas?
There's no right hand side to your const declaration. It's not legal javascript to do just this:
const foo;
You need to also give it a value, as in
const foo = "bar";
It looks like you're trying to do destructuring of a state object. In function components, it's common to split up your state instead of having it be in a single object, so you may not want to do this destructuring statement at all, but instead do independent states. For example:
const [editingFormVisible, setEditingFormVisible] = useState();
const [editingAppointment, setEditingAppointment] = useState();
const [data, setData] = useState();
// ... etc
If you don't want to split up the state and want to keep doing the destructuring assignment, then put the object to destructure on the right hand side.
const {
editingFormVisible,
editingAppointment,
data,
addedAppointment,
isNewAppointment,
previousAppointment,
} = someObject; // <------- added
I want to insert some props to a React component which I have extracted out of props.children, like so:
<PageContainer>
<PageOne />
<PageTwo />
<PageThree />
</PageContainer>
Inside <PageContainer> i am extracting the current page via props.children and current page index, something like this:
const { children, pageIndex } = props;
let activePage = React.Children.toArray(children)[pageIndex];
Inside this PageContainer I have the "oportunity" to send down a function that I need inside the <PageOne>, <PageTwo> and <PageThree>. I tried something like this, but then I got some compiling problems, I think. Since this worked locally and not in my test environment:
const newProps = React.cloneElement(activePage.props.children, { myWantedFunction: myWantedFunctionThatIsAvailableInsidePageContainer });
activePage = React.cloneElement(activePage, { children: newProps });
The problem here is that myWantedFunction is not working in the test environment, but it is working locally. It says that it is not a function in the test environment, but when I console.log it out locally, it prints out a function. I am guessing there is a compiling problem, but I am wondering if there is a better way to acheive this (send down props to a component stored in a variable that I got out of props.children)?
Thank you in advance!
You are almost correct, you need to use React.cloneElement() to send props down to the child component. First you need to get the active page which you're doing right:
let activePage = React.Children.toArray(children)[pageIndex];
Then you need to use React.cloneElement to pass props to this component like this. Let's the name of the prop is isAuthenticated which is a boolean:
let component = React.cloneElement(activePage, { isAuthenticated: true })
Now in your page you'll be able to access this prop: prop.isAuthenticated
I created a working demo for you, have a look:
https://codesandbox.io/s/determined-kowalevski-eh0ic?file=/src/App.js
Hope this helps :)
Alternatively you could also do something like this:
const PAGES = [PageOne, PageTwo, PageThree]
function PageContainer(props){
const {pageIndex} = props
const ActivePage = PAGES[pageIndex]
return <ActivePage someFunction={someFunction} />
function someFunction(){
// the function you want to pass to the active page
}
}
Node that ActivePage has a capital A, which allows it to be used as a JSX component.
A drawback of this solution is however that if you use typescript it wont type check the props correctly since you only know which component is to be rendered at runtime. You could replace the array lookup with a switch to avoid that issue.
Yet another variation would be to just let the Page components handle their own state and pass the function to all of them. Like this:
function PageContainer(props){
const {pageIndex} = props
const someFn = () => 0
return <React.Fragment>
<PageOne id={1} activePage={pageIndex} someFunction={someFn} />
<PageTwo id={2} activePage={pageIndex} someFunction={someFn} />
<PageThree id={3} activePage={pageIndex} someFunction={someFn} />
</React.Fragment>
}
then in the Page Components just check if the pageIndex corresponds to their id:
function PageOne(props){
const {id, activePage, someFunction} = props
if (id === activePage){
const result = someFunction()
return <div>Page One: {result}</div>
}
}
I have a parent component that having some props passing from grandparent component and I am using one prop (object) and pass the value of that object to children component as props. I also pass a function to child component in order to get the updated value back from child component.
ParentComponent.js
const ParentComponent = props => {
const { record, saveRecord } = props;
const editedRecord = {...record}
const handleRecordValues = (name, value) => {
editedRecord[name] = value;
};
...
const content = <div>
<ChildComponent name={record.name} value={record.value} setValue={handleRecordValues} />
<Button onClick={() => saveRecord(editedRecord)} />
</div>
return content;
}
ChildrenComponent.js
const ChildComponent = props => {
const { name, value, setValue } = props;
const [input, setInput] = useState(value);
const handleChange = (e, text) => {
setInput(text);
setValue(name, value);
}
return <TextField value={input} onChange={handleChange}/>
}
Above are the sample components I have. The issue is when I pass the editedRecord to saveRecord func to grandparent component the editedRecord is always the same as record as it is copied from record and value is not updated for that variable. I expect the editedRecord being updated by the handleRecordValues func.
For example, the record that I get is {}. And I create a new const editedRecord which is also {}.
After I input some value from ChildComponent the editedRecord should be updated to {name: value}. However when I click on Button in ParentComponent the editedRecord parameter is still {}.
Updated
Instead of using const I use
const [editedRecord, setEditedRecord] = useState(record);
const handleRecordValues = (name, value) => {
const newRecord = {
...editedRecord
};
newRecord[name] = value;
setEditedRecord(newRecord);
};
Now the editedRecord value got updated but another issue came up:
when I have multiple components as child components it only update the last one entry I have entered.
Your setValue/handleRecordValues function changes a variable ... but React has no way of knowing when that variable changes.
To let React know, you have to call saveRecord(editedRecord) after you make the change, or in other words you have to invoke a state-setting function, so that React knows about the change.
In general in React, if you don't change context/state/props (and for context/state, that means doing so using the appropriate React functions), React can't know to re-render your components in response. This means that any data that your components depend on to render needs to be changed via one of those three mechanisms, not just via ordinary Javascript, ie. a.b = c.
EDIT: To clarify a point in the comments. When you make a state variable:
const [myState, myStateSetter] = useState('');
there is nothing "magic" about myState; it's just another JS variable. Javascript doesn't give React any way to know when that variable changes, so if you just do:
myState = 4;
React has no idea that you did so. It only knows that it changed if you tell it that it changed ... ie. if you call:
myStateSetter(4);
Here's how I would alter the parent component to make everything work with react. The main issue you were having is that react needs to know that a change has occurred, so we need to set up the values as state/set state.
const ParentComponent = props => {
const { record, saveRecord } = props;
const [editedRecord,setEditedRecord] = useState(record);
useEffect(()=>{
//This sets the record straight whenever the parent passes a new record.
//You'd need to make sure the record is referentially stable when it isn't being updated, though
setEditedRecord(record);
},[record])
const handleRecordValues = (name, value) => {
setEditedRecord(record=>{...record,[name]:value});
};
...
const content = <div>
<ChildComponent name={editedRecord.name} value={editedRecord.value} setValue={handleRecordValues} />
<Button onClick={() => saveRecord(editedRecord)} />
</div>
return content;
}