Reputation: 67
So let's say I have a react component that is connected to redux store, but also manages some internal state.
The internal state contains data
to be fetched, and loading
flag which is used to render either a 'Loading...' text or the data after it's fetched.
I also need selectedItems
which is something I get from the redux store. These items are also used in other parts of the app, e.g. a sidebar showing currently selected items, so the user always knows which items are selected and can use the sidebar to select or remove items.
In this component, selectedItems
are used to help map over the data that I fetch.
So the component looks something like this:
export default () => {
const selectedItems = useSelector(state => state.itemsReducer.selectedItems);
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData('someApi/data').then(res => {
setData(res);
setLoading(false);
});
}, [selectedItems]);
return (
<div>
{loading ? (<div>Loading...</div>) : (<div>{data}</div>)}
</div>
);
}
Now let's say that in the part <div>{data}</div>
I actually do some complex mapping, where I use information from selectedItems
to properly map over the data.
The problem I have is this: if user adds an item through the sidebar, which then changes selectedItems
in the store, that means that the current data
is no longer valid and I need to fetch new data. So immediately after the store change I would like to set loading
to true
to show 'Loading...' text and then fetch new data after the component mounts, so in useEffect
. So, I know that useEffect
is called only after the component renders. However, I am mapping over data
in the JSX, which uses selectedItems
that have already been updated based on the store change, but data
is still the old one as useEffect
hasn't been called yet. I do NOT want to map over data
at all during the time between selectedItems
has been changed and new data has been fetched. So I basically want to have loading
set to true in between these two events, but how would I go about doing that? Setting it in the useEffect
is too late as the component has already been rendered/updated, but before that, it already knew about the change in selectedItems
(which actually causes a bug as it tries to access some properties in data
based on the new selected item, but data
is still the old one so it does not contain that.
This whole thing seems like a common case for a react app to deal with so I thought I would find a solution quickly, but well, no success yet.
UPDATE based on the answer from @brendangannon
So I might have provided a too simplified example. By complex mapping I meant mapping the data to JSX. First I map over data
and that has a nested map over selectedItems
, it creates a table-like structure where rows are based off of data
, and apart from first column, number of columns is based off of selectedItems
.
One thing I didn't mention but now seems relevant, is that in actuality the data
gets to the component by another hook.
So instead of the useEffect
above, I have something like this:
export default () => {
// ...
const {data, loading} = useLoadData(...someParams);
// ...
}
The hook keeps an internal state and fetches the data in its own useEffect
that has a dependency on selectedItems
, and fetches new data if selectedItems
change, based on those items.
Would it make sense to keep internal state also in my component, which would be the copy of data
, let's name it dataToMap
for now, that would be used in the JSX? And I would also add a useEffect
with a dependency [data]
, that would set the state for this component when new data is loaded basically updating dataToMap
. So if selectedItems
changed, this component would re-render while using the stale dataToMap
for now, then the internal useEffect
of useLoadData
would start loading the new data internally while giving me loading === true
, which the upcoming render will use to show 'Loading...' text, and then when fresh data
is fetched, my hook fires due to [data]
dependecy changing, sets new dataToMap
to internal state, which triggers yet another re-render that can (and will, due to loading===false
set internally by useLoadData
) now safely map over the new data in state.
So the component would look something like this:
export default () => {
// ...
const selectedItems = useSelector(state => state.itemsReducer.selectedItems);
const {data, loading} = useLoadData(...someParams);
const [dataToMap, setDataToMap] = useState([]);
useEffect(() => {
if (loading === false) {
// new data fetched
setDataToMap(data);
}
}, [data]);
return (
<div>
{loading ? (<div>Loading...</div>) : (
<div>
{dataToMap.map(dataEl => {
return (
<>
// render some row stuff
{selectedItems.map(selItem => {
// render some column stuff
})}
</>
)
})}
</div>
)}
</div>
);
}
That leaves me with two questions:
useLoadData
hook, is this a good approach?useLoadData
?UPDATE 2
Just realized this would lead to the very same problem - when stale dataToMap
is mapped over while selectedItems
already changed (new item present) it would try to access data that is not in dataToMap
.
I'm thinking I'll just store selectedItems
internally in useLoadData
state and return it to my component for the mapping e.g. selectedItemsForMapping
. When selectedBuckets
change, it would still use the old selectedBucketsForMapping
for the render over stale data and after that it would return new selectedItemsForMapping
along with loading===true
, that way I could show 'Loading...' and the rest has been discussed above.
Upvotes: 0
Views: 1012
Reputation: 2652
I think you're going to want to change your design a bit. The 'complex mapping' shouldn't (as a rule) happen in render -- you wouldn't want to re-do that mapping on every render regardless of how you manage state. It's best to calculate that derived data at the point where the original data changes (in this case, a result from an API call), and have the data ready to use at the point it 'enters' the component (in this case, when it's set to state). Keeping data processing separate from rendering is a good example of single/separate responsibilities, and makes testing easier.
So it might look something like:
export default () => {
const selectedItems = useSelector(state => state.itemsReducer.selectedItems);
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData('someApi/data').then(res => {
const modifiedData = doComplexMapping(res, selectedItems);
setData(modifiedData);
setLoading(false);
});
}, [selectedItems]);
return (
<div>
{loading ? (<div>Loading...</div>) : (<div>{data}</div>)}
</div>
);
}
In this case, all render
does is render something from state, so it can safely execute as long as there is data in state, whether it is stale or not. When selectedItems
changes in the store, the component will render again (showing old data), then immediately set a 'loading' state, rendering the loading UI, and then it will fetch the new data, process it, set it in state, and render the new data. The first two renders will happen fast enough that you probably won't notice the re-render of stale data; it will show the 'loading' UI more or less immediately.
However, since any change to selectedItems
triggers a new api call, it might make sense to move this api call out of this component and into the action creator that changes selectedItems
. Whatever complex mapping is currently happening in this component would also move to the action creator, and the derived data you are currently generating in this component would instead be set in the redux store, and this component would pull it out via a selector.
This way, the change to application state (selectedItems
have changed due to user interaction) is directly connected to re-fetching data (via an api call), rather than indirectly, via a side effect of component render downstream of the state change. By changing the derived data at the same place where the original state change happens, the code more clearly reflects the cause-and-effect nature of the app. It depends on the design of the app as a whole, but in react/redux apps it is common for data fetching to happen at the action creator level in this way, especially when there are multiple UI components involved in changing and rendering the related state.
Upvotes: 1