React + Redux - Update local state on store change => infinity loop - reactjs

I have an array that resides in store, let's call it users and I have a custom hook that retrieves it from store (via useSelector). Based on this array in the store, I have a local state in my component that I use as the display source of a table. I need to have it separately in local state since I filter the data based on a search functionality.
What I want to do is, every time when the data in the store changes, I want to update the local state as well to reflect those changes (but call the filtering function on top of it before hand).
This however results in an infinity loop since the useEffect + setState cause a redraw which changes the store variable again etc.
const users = useUsers() // uses redux store
const [displayUsers, setDisplayUsers] = useState(users) // local state
useEffect(() => {
const filteredUsers = onSearch(users) // applies the filtering
setDisplayUsers(filteredUsers) // updates the local state
}, [users])
return <Table source={displayUsers} ... />
Can anybody help?

Since displayUsers is derived from the props, you don't need to maintain it in the state. Calculate it on the fly whenever the props changes, and use memo if the component is rendered often for other reasons:
const users = useUsers() // uses redux store
const displayUsers = useMemo(() => onSearch(users), [onSearch, users])
return <Table source={displayUsers} ... />
If you really need to set the state when the store changes, make sure that the selector is memoized - ie if nothing changes you'll get the same array. So when re-rendering the component, the selector would return the same users array, and this will prevent the useEffect from being called again.
You can call useSelector with the equalityFn (2nd param) to avoid returning a new value if nothing changed (see Equality Comparisons and Updates). This is the example from the docs:
import { shallowEqual, useSelector } from 'react-redux'
const selectedData = useSelector(selectorReturningObject, shallowEqual)

Related

Init UseState value from UseContext value

I have context and state variables. My state variable is initialized with my context variable. When I update my context in another component for exemple: changing the player's action (attack to defend), state variable keep the previous value.
const [player,setPlayer] = useContext(PlayerContext);
const [action, setAction] = useState(player.action);
useEffect(() => {
console.log(action); // => attack
console.log(player.action); // => defend
});
This must surely be a rendering problem.
If you want the action state variable to reflect updates to player.action, you need make sure your dependency array (the second parameter to useEffect) includes player.action (so that it only runs when the value changes), and then call setAction explicitly:
useEffect(() => {
if (player.action !== action) {
setAction(player.action);
}
}, [player.action])
The React documentation has an extensive guide on Using the Effect Hook that might be helpful, along with the useEffect hook API reference.
Your context state is changed somewhere after your component is rendered. Therefore the component state is not in sync with the context state anymore. With your browsers debugger you can check this behaviour and find out where it is changed.
As a general rule you should avoid adding a component state, when it shall only mirror a context‘s state, consume the context directly inside your component.

Call Hooks on component change in React Native

I'm working on an react native app.
This app use a database, the main component use 2 differents hook.
The first hook retrieves the results of a SQL query and store them in a variable.
The second hook creates a list from the first variable
Like this:
const [people, setPeople ] = useState([]);
useEffect (() => {
db.getAllPeople().then(row => setPeople(row))
},[])
const [listData, setListData] = useState([]);
useEffect(()=> {
setListData(
Array(people.length)
.fill('')
.map((_, i) => ({ key: `${i}`, name: `${people[i].name}`}))
)
}, [people]);
After that, my main component displays a SwipeList from the results.
Here is the problem. I am using another component to add an element to my database. When I return to my main component I would like this new element to be displayed. But the problem is that the 2 hooks are not called on the component change and the list therefore remains unchanged.
I've tried to use the useFocusEffect but it doesn't work in my case.
Any suggestions ?
I think the useState hook manages the state of the component itself, unless you are passing this state among your parent and child or using callbacks to set the state on the component that you want to render, you could use a single source of truth to handle the changes in data, react itself will notice this changes and therefore, render the changed screens, considering that you have asynchronous operations when querying the database, a combination of redux and redux saga may help you.
https://github.com/redux-saga/redux-saga
There're one issues with your current code, or potential issues
Your second useEffect might get called when people becomes an empty list, this will reset your list data. The cure is to put a if statement inside, ex.
useEffect(()=> {
if (!people) return;
setListData(...)
}, [people]);
To be honest, if these two lists are connected, you shouldn't use two hook. The best way is to define listData
const listData = (a function that takes people as input), ex.
const listData = people.map(v => v)
Of course, there might be a reason why you'd like to introduce more hook in complex situation, ex. useRef, useMemo.

Setting Redux state as a default state when using React Hooks

I have a redux action that fetches all data and stores it into a global Redux store.
I want to store that state in a local state using Hooks so that the actual state doesn't get changed when I change it locally.
What I am doing right now is,
const [filteredTable, setFilteredTable] = useState([])
useEffect(() => {
props.fetchDatabase();
props.fetchOptions();
setFilteredTable(props.filtered_table_data);
}, [])
In useEffect, the props.fetchDatabase() gets the props.filtered_table_data and I can see that when I console.log it out.
However, when I use Hooks to store it into a local state and check if it's in there,
console.log(filteredTable, 'filteredTable')
just gives me [].
What am I doing wrong?
I believe the props.fetchDatabase() call is asynchronous, so by the time you are attempting to setFilteredTable the props.filtered_table_data has not updated yet.
You can try something like this:
useEffect(() => {
props.fetchDatabase();
props.fetchOptions();
}, [])
useEffect(() => {
setFilteredTable(props.filtered_table_data);
}, [props.filtered_table_data]);
Note that this effect will run every time filtered_table_data changes, so you may need to wrap around the code in the callback with some sort of condition if you want to restrict setting the local state.
useEffect's callback with [] as hook's second argument is only being called once when component just mounted. Inside it fetchDatabase, and fetchOptions callbacks are called, and right after that (when data isn't yet fetched) you call setFilteredTable, that's why there are empty array occurs in filteredTable.
Not sure if this answers your question, but React-Redux provides some hooks for accessing the redux store.
The one that might be of interest to you is the useSelector() hook.
Here's an example usage:
import { useSelector } from 'react-redux';
const App = () => {
const tableData = useSelector(state => state.tableData);
...
}

Change internal component state after redux store change but before component renders

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:
If I keep the useLoadData hook, is this a good approach?
Is there a better approach for this all considering possibly removing 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.
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.

What is the best-practice for using redux state inside components?

Lets say I have a large store object, and I only need a small slice of it per component. what is the best practice for managing the state inside each component?
the two approaches i have considered, are either setting the internal component state to the slice of state that i need in componentDidMount(), or assigning a const to the slice in render(). what would be the benefit/drawback of each? they both update whenever something is changed, and all of my changeHandlers call actions on the store, instead of modifying component state directly. just to be very specific, i'm using mapStateToProps and only need to assign the slice to some internal variable because accessing the slice requires an object.map, and I only want to have to do that once per component, instead of every time i need to access the state (if i could just do this.props.storeName.someProp.value every time, i would)
setting to internal state
class LoginPrimaryCell extends Component {
state = {
data: this.props.storeFeatures.data.find(element => element.feature === 'login').settings
}
componentDidMount() {
this.setState({ data: {...this.props.storeFeatures.data.find(element => element.feature === 'login').settings}})
}
handleChange = (event, name) => {
this.props.editFeature({feature: 'login', setting:'checkbox', value: event.target.checked}) //editFeature is an action on store
}
render() {
...rest of component...
assigning to a const
class LoginPrimaryCell extends Component {
handleChange = (event, name) => {
this.props.editFeature({feature: 'login', setting:'checkbox', value: event.target.checked}) //editFeature is an action on store
}
render() {
const data = {...this.props.storeFeatures.data.find(element => element.feature === 'login').settings}
}
All i'm looking for, is the best practice for this situation, and what the benefits/downsides are to each approach. alternatively, if there is virtually no difference, and it's all personal preference. Thanks!
Ideally the best place to filter/find elements would be to use a memoized selector in mapStateToProps instead of passing the entire data to component and filtering in it, unless you want to filter on a condition that is being set within state of the react component.
If however, there is a case that you want to filter within the component, it needs to be done within the render method using a memoized function since it will avoid computation if the same arguments are passed on consecutive renders and would not need handling of the props or filter change at any other location.
Setting the filtered data to state is not a very correct solution since you won't be updating this state often and also, you would need to update this state when your storeFeatures props or the filter condition change, which is all a bit cumbersome

Resources