In my homepage, I have code something like this
{selectedTab===0 && <XList allItemList={some_list/>}
{selectedTab===1 && <YList allItemList={some_list2/>}
Now, In XList, I have something like this:
{props.allItemList.map(item => <XItem item={item}/>)}
Now, Inside XItem, I am calling an api to get the image of XItem.
Now my problem is When In homepage, I switched the tab from 0 to 1 or 1 to 0, It is calling all the API's in XItem again. Whenever I switched tab it calls api again. I don't want that. I already used useEffect inside XItem with [] array as second parameter.
I have my backend code where I made an api to get the image of XItem. The API is returning the image directly and not the url, so I can't call all api once.
I need some solution so that I can minimize api call.
Thanks for help.
The basic issue is that with the way you select the selected tab you are mounting and unmounting the components. Remounting the components necessarily re-runs any mounting useEffect callbacks that make network requests and stores any results in local component state. Unmounting the component necessarily disposes the component state.
{selectedTab === 0 && <XList allItemList={some_list} />}
{selectedTab === 1 && <YList allItemList={some_list2} />}
One solution could be to pass an isActive prop to both XList and YList and set the value based on the selectedTab value. Each component conditionally renders its content based on the isActive prop. The idea being to keep the components mounted so they only fetch the data once when they initially mounted.
<XList allItemList={some_list} isActive={selectedTab === 0} />
<YList allItemList={some_list2} isActive={selectedTab === 1} />
Example XList
const XList = ({ allItemList, isActive }) => {
useEffect(() => {
// expensive network call
}, []);
return isActive
? props.allItemList.map(item => <XItem item={item}/>)
: null;
};
Alternative means include lifting the API requests and state to the parent component and passing down as props. Or using a React context to do the same and provide out the state via the context. Or implement/add to a global state management like Redux/Thunks.
Just to quickly expand on Drew Reese's answer, consider having your tabs be children of a Tabs-component. That way, your components are (slightly) more decoupled.
const MyTabulator = ({ children }) => {
const kids = React.useMemo(() => React.Children.toArray(children), [children]);
const [state, setState] = React.useState(0);
return (
<div>
{kids.map((k, i) => (
<button key={k.props.name} onClick={() => setState(i)}>
{k.props.name}
</button>
))}
{kids.map((k, i) =>
React.cloneElement(k, {
key: k.props.name,
isActive: i === state
})
)}
</div>
);
};
and a wrapper to handle the isActive prop
const Tab = ({ isActive, children }) => <div hidden={!isActive}>{children}</div>
Then render them like this
<MyTabulator>
<Tab name="x list"><XList allItemList={some_list} /></Tab>
<Tab name="y list"><YList allItemList={some_list2} /></Tab>
</MyTabulator>
My take on this issue. You can wrap XItem component with React.memo:
const XItem = (props) => {
...
}
const areEqual = (prevProps, nextProps) => {
/*
Add your logic here to check if you want to rerender XItem
return true if you don't want rerender
return false if you want a rerender
*/
}
export default React.memo(XItem, areEqual);
The logic is the same if you choose to use useMemo.
If you want to use useCallback (my default choice) would require only to wrap the call you make to the api.
Related
Let's say that I have this component:
const Test = ({ children, ...rest }) => {
return <>{children}</>
};
export default Test;
I am wondering if it is possible to create a variable that holds the component like this:
const test = <Test></Test>;
And then loop over some data and push children to the test variable on every iteration.
if you don't have the data yet, then all you have to do is conditionally render your component when you do have the data.
{ data ? (<Test>{data.map(...)}</Test>) : <SomeOtherComponent /> /* or null */}
or
{ data ? <>{data.map((x) => <Test>{x}</Test>)}</> : <SomeOtherComponent /> /* or null */}
depending on what you want achieve, i didn't fully understand your question
i.e. if you have the data you need, render the component, rendering the children as you see fit, otherwise render some other component (or null, to render nothing)
Yeap, try that pattern:
const test = (children) => <Test>{children}</Test>;
and usage
<>
{[1,2,3].map(el=>test(el))}
</>
[Edited]
const TestComp = ({children}) => <Test>{children}</Test>;
<>
{[1,2,3].map(el=>(<TestComp>{el}</TestComp>))}
</>
When the component first renders it gets from the switchcase (enters ALL case because its the default value) on the usePositions hook, the value for positions and I set them there, and return them to the Positions controller.
The problem comes when the selectedClient changes from ALL to TODAY (its a context value, I change it in a Sidebar component somewhere else) and before entering the switch case in TODAY value to get the positions of today, I noticed the Positions component already rendered the old state of ALL positions again! Then a second later it renders correctly the todays positions.
I noticed this because my browser on my network tab shows some calls to the server that IndividualPosition makes it means that it rendered
This is my component where it calls the usePositions hook
export const Positions = () => {
const { selectedClient } = useSelectedClientValue();
const { loading, setLoading } = useLoadingValue();
const { positions } = usePositions(selectedClient);
const clientName =
typeof selectedClient === "string" ? selectedClient : selectedClient.name;
useEffect(() => {
setLoading(false);
}, [positions]);
let positionsView = (
<ul className="positions__list">
{positions.map((position) => (
<IndividualPosition position={position} key={position.positionId} />
))}
</ul>
);
return (
<div className="positions" data-testid="positions">
{!loading ? (
<>
<div className="positions__header">
<h2 data-testid="client-name">{clientName}</h2>
</div>
{positionsView}
</>
) : (
<div className="loading-main-window">
<Spinner />
</div>
)}
</div>
);
};
This is the hook where I fetch the data
export const usePositions = selectedClient => {
const [positions, setPositions] = useState([]);
const {setLoading} = useLoadingValue()
useEffect(() => {
setLoading(true);
switch (selectedClient) {
case 'ALL':
getPositions().then(pos => {
setPositions(pos);
});
break;
case 'TODAY':
getTodayPositions().then(pos => {
setPositions(pos);
});
break;
default:
break;
}
}, [selectedClient]);
return {positions, setPositions};
};
The useEffect runs each time the selectedClient change
It looks like the component renders again before getting the todays data and thats why it shows the old state before getting the new data, but I thought that could be avoided with the loading flag
Basically:
-Positions renders, the hook fetches allPositions, its fine
-If I change in the sidebar the value of the selectedClient context value, the Positions components renders again, rendering the IndividualComponent but with the state of allPositions.
- Instead it should wait till todaysPositions fetches to show the new state (loading should do that)
I already tried having a loading local state (my loading is a context value)
Moving the loading in the useEffects on my local component instead of my hook
Setting loading to false inside the hook after fetching the data
Any ideas?
I've tried to ask this ungooglable to me question dozens of times. I've made almost the simpliest example possible to ask this question now.
I change the value of the hook in the handleChange method. But then console.log always shows previous value, not new one. Why is that?
I need to change the value of the hook and then instead of doing console.log use it to do something else. But I can't because the hook always has not what I just tried to put into it.
const options = ["Option 1", "Option 2"];
export default function ControllableStates() {
const [value, setValue] = React.useState(options[0]);
const handleChange = val => {
setValue(val);
console.log(value);
};
return (
<div>
<div>{value}</div>
<br />
<Autocomplete
value={value}
onChange={(event, newValue) => {
handleChange(newValue);
}}
options={options}
renderInput={params => (
<TextField {...params} label="Controllable" variant="outlined" />
)}
/>
</div>
);
}
You can try it here.
https://codesandbox.io/s/awesome-lumiere-y2dww?file=/src/App.js
I believe the problem is that you are logging the value in the handleChange function. Console logging the value outside of the function logs the correct value. Link: https://codesandbox.io/s/async-fast-6y71b
Hooks do not instantly update the value you want to update, as you might have expected with classes (though that wasn't guaranteed either)
State hook, when calling setValue will trigger a re-render. In that new render, the state will have the new value as you expected. That's why your console.log sees the old value.
Think of it as in each render, the state values are just local variables of that component function call. And think as the result of your render as a result of your state + props in that render call. Whenever any of those two changes (the props from your parent component; the state, from your setXXX function), a new render is triggered.
If you move out the console.log outside of the callback handler (that is, in the body of your rendered), there you will see in the render that happens after your interaction that the state is logged correctly.
In that sense, in your callbacks events from interactions, you just should worry about updating your state properly, and the next render will take care to, given the new props/state, re-render the result
The value doesn't "change" synchronously - it's even declared with a const, so even the concept of it changing inside the same scope doesn't make sense.
When changing state with hooks, the new value is seen when the component is rerendered. So, to log and do stuff with the "new value", examine it in the main body of the function:
const ControllableStates = () => {
const [value, setValue] = React.useState(options[0]);
const handleChange = val => {
setValue(val);
};
// ADD LOG HERE:
console.log('New or updated value:', value);
return (
<div>
<div>{value}</div>
<br />
<Autocomplete
value={value}
onChange={(event, newValue) => {
handleChange(newValue);
}}
options={options}
renderInput={params => (
<TextField {...params} label="Controllable" variant="outlined" />
)}
/>
</div>
);
}
You're printing out the old value in handleChange, not the new val.
i.e.
const handleChange = val => {
setValue(val);
console.log(value);
};
Should be:
const handleChange = val => {
setValue(val);
console.log(val);
};
Actually, lets get a little back and see the logic behind this scenario.
You should use the "handleChange" function ONLY to update the state hook, and let something else do the logic depends on that state hook value, which is mostly accomplished using "useEffect" hook.
You could refactor your code to look like this:
const handleChange = val => {
setValue(val);
};
React.useEffect(() => {
console.log(value);
// do your logic here
}, [value])
So I think that the main problem is that you're not understanding how React
deals with components and states.
So, I'll vastly simplify what React does.
React renders a new component and remembers it's state, it's inputs (aka
props) and it's the state and inputs of the children.
If at any given point an input changes or a state changes, React will render
the component again by calling the component function.
Consider this:
function SomeComponent(text) {
return (<div>The <i>text</i> prop has the value {text}</div>)
}
Let's say the initial prop value is "abc", React will call SomeComponent("abc"), then the function returns
<div>The <i>text</i> prop has the value abc</div> and React will render that.
If the prop text does not change, then React does nothing anymore.
Now the parent component changes the prop to "def", now React will call
SomeComponent("def") and it will return
<div>The <i>text</i> prop has the value def</div>, this is different from
last call, so React will update the DOM to reflect the change.
Now let's introduce state
function SomeComponent() {
const [name, setName] = React.useState("John")
function doSomething()
{
alert("The name is " + name)
}
return (
<p>Current name: {name}</p>
<button onClick={() => setName("Mary")}>Set name to Mary</button>
<button onClick={() => setName("James")}>Set name to James</button>
<button onClick={() => doSomething()}>Show current name</button>
)
}
So here React will call SomeComponent() and render the name John and the 3
button. Note that the value of the name variable does not change during the
current execution, because it's declared as const. This variable only reflects
the latest value of the state.
When you press the first button, setName() is executed. React will internally store
the new value for the state and because of the change of state, it will render
the component again, so SomeComponent() will be called once again. Now the variable name will
reflect again the latest value of the state (that's what useStatedoes), so in this case Mary. React
will realize that the DOM has to be updated and it prints the name Mary.
If you press the third button, it will call doSomething() which will print the
latest value of the name variable because every time React calls
SomeComponent(), the doSomething() function is created again with the latest
value of name. So once you've called setName(), you don't need to do
anything special to get the new value. React will take care of calling the
component function again.
So when you don't use class components but function components, you have to think
differently: the function gets called all the time by React and at any single
execution it reflects the latest state at that particular point in time. So when you
call the setter of a useState hook, you know that the component function will
be called again and useState will return the new value.
I recommend that you read this article, also read again Components and
Props from the React documentation.
So how should you do proceed? Well, like this:
const options = ["Option 1", "Option 2"];
export default function ControllableStates() {
const [value, setValue] = React.useState(options[0]);
const handleChange = val => {
setValue(val);
console.log(value);
};
const handleClick = () => {
// DOING SOMETHING WITH value
alter(`Now I'm going to do send ${value}`);
}
return (
<div>
<div>{value}</div>
<br />
<Autocomplete
value={value}
onChange={(event, newValue) => {
handleChange(newValue);
}}
options={options}
renderInput={params => (
<TextField {...params} label="Controllable" variant="outlined" />
)}
/>
<button type="button" onClick={handleClick}>Send selected option</button>
</div>
);
}
See CodeSandbox.
I am developing a web app in React. I am trying to do the following:
Get user information from the database and store it using the Context API.
Render the component when the context changes.
The problem is that there is apparently a time lag in updating the context and so the component does not re-render. How do I make sure that the component re-renders after the context value is updated?
Below is the code I am using:
const User = () => {
const {userinfo, setuserinfo} = useContext(Userinfo) //Declare useContext hooks
useEffect(()=>{
/*
Compute the value of mid
*/
setuserinfo(mid); //Set 'userinfo' using context API
},[])
return (
<>
{userinfo.map(ui =>
//JSX to map userinfo to UI components
)}
</>
)
}
Thanks a lot in advance!
You could check or verify userinfo before attempting to map it to rendered components.
E.g.
// or use lodash.isNil, or lodash.isEmpty, or whatever check you want
const isNil = o => o === null || o === undefined;
return ( isNil(userInfo) ?
<span>Loading...</span> :
<>
{userinfo.map(ui =>
//JSX to map userinfo to UI components
)}
</>
);
You can try something like this.
return userinfo && Array.isArray(userinfo) && userinfo.length ? (
<>
{userinfo.map(ui =>
//JSX to map userinfo to UI components
)}
</>
) : <span>Loading... (or whatever)</span>;
How I do it right now
I have a list of items. Right now, when the user presses button X, shouldShowItem is toggled. shouldShowItem ultimately lies in redux and is passed down into Item as a prop. it's either true or false. When it changes, toggleDisplay is called and changes state in my hook:
useEffect(() => {
toggleDisplay(!display); //this is just a useState hook call
}, [shouldShowItem]); //PS: I'm aware that I don't need this extra step here, but my actual code is a bit more complicated, so I just simplified it here.
My Problem is, that I have one single shouldShowItem property in redux, not one shouldShowItem for each item. I don't want to move this property into the redux-state for each and every item.
Problem:
The problem with my construction however is that shouldShowItem is being saved, which means that if I toggle it at time X for item Y, and then my Item Z also re-renders as a result of an unrelated event, it will re-render with an updated shouldShowItem state, - although that state change was intended for Item X.
Essentially, I am saving the state of shouldShowItem in redux while I just need a toggle, that I can dispatch once, that works on the current Item, and then isn't read / needed anymore. I want to basically dispatch a toggle, - I don't care about the state of each item within redux, I just care that it's toggled once.
Suggestions?
Edit: More Code
Item:
const Item = ({shouldShowItem, itemText, updateCurrentItem})=>
{
const [display, toggleDisplay] = useState(false);
useEffect(() => {
if (isSelected) {
toggleDisplay(!display);
}
}, [shouldShowItem]);
return (
<div
onClick={() => {
toggleDisplay(!display);
updateCurrentItem(item._id);
}}
>
{display && <span>{itemText}</span>}
</div>
);
}
Mapping through item list:
allItems.map((item, i) => {
return (
<Item
key={i}
shouldShowItem={this.props.shouldShowItem}
itemText={item.text}
updateCurrentItem={this.props.updateCurrentItem}
);
});
The props here come from redux, so shouldShowItem is a boolean value that lies in redux, and updateCurrentItem is an action creator. And well, in redux i simply just toggle the shouldShowItem true & false whenever the toggle action is dispatched. (The toggle action that sets & unsets true/false of shouldShowItem is in some other component and works fine)
instead of a boolean shouldShowItem, why not convert it into an object of ids with boolean values:
const [showItem, setShowItem] = useState({id_1: false, id_2: false})
const toggleDisplay = id => {
setShowItem({...showItem, [id]: !showItem[id]})
updateCurrentItem(id)
}
allItems.map((item, i) => {
return (
<Item
key={i}
shouldShowItem={showItem[item._id]}
itemText={item.text}
updateCurrentItem={toggleDisplay}
);
});
const Item = ({shouldShowItem, itemText, updateCurrentItem}) => {
return (
<div
onClick={() => toggleDisplay(item._id)}
>
{shouldShowItem && <span>{itemText}</span>}
</div>
)
}