useSelector can not be called inside a callback - reactjs

I'm very new with redux / toolkit.
I need fetch data using the redux hook useSelector in my react functional component but the data isn't quickly ready so it seems that useSelector will return an empty array at first , then when the data is ready , it will return the popuplated array of data.
At first , i thought useSelector could be asynchronous but it's not. Redux re-render my component each time the data i'm looking for gets its value changed and that's not suitable for my functional needs. In some cases i don't want my component to re-render (espcially when only just an atomic part of the data changes, that strangely re-render all my component as well).
What i tried first is to make my data as part of my component's state so i can decide when i want it to be updated, i made the call to useSelector inside useState as it accepts a callback
const [data, setData] = useState(()=> useSelector(...))
but i got the error :
useSelector can not be called inside a callback
Any suggestions ?

You can use useEffect.
const selectorData = useSelector(...);
const [data, setData] = useState(selectorData);
useEffect(() => {
setData(selectorData)
}, [selectorData])
If you do it like that, component doesn't re-render every time, it will just re-render when your selector data came.

Related

How does Redux useSelector affect on react component rendering?

I don't understand how does my component Word will rerender. I have a redux state keeping my {history: {letters}} state. So the question is: If {letters} are passed into useEffect deps array, will my component Word rerender if {words} property is changed?
`
function Word() {
const { history: {letters, words} } = useAppSelector(state => state)
useEffect(() => {
}, [letters])
return (
<div>
</div>
)
}
`
I expect my component rerender only if letters are changed.
You are maintaining a state using Redux. So, the component re-renders if any state used in the component itself is changed. In your case, your Word component will re-render if letters or words or both got changed. That's how it works.
BTW, your useEffect should only be triggered upon any change in letters only since you have included only letters in its dependency array.
If you want to optimize the performance, you can memorize things using useMemo and useCallback wherever necessary. Pass the dependencies correctly to recalculate them only upon required state changes.
UseSelector will already rerender if the selected part of the state has change. you dont have to add [letters] on useEffect
UseSelector is subscribed to the state and if detects any change, it calls checkForUpdates
subscription.onStateChange = checkForUpdates
and checkForUpdates checks if the reference of the state changed, if it did. it calls forceRender function so your component renders.
github useSelector
useEffect is used to dispatch actions to populate the state before the component first renders so useSelector will have access to newly populated the state

Functional component - calling useEffect on state change, but not when props changed

Working with a functional component which has the following two useEffects:
//update the state when props changed
useEffect(() => {
const newState = mapPropsToState(props);
if (!_.isEqual(newState, state)) {
setState(newState);
}
}, [props]);
//make an API call
useEffect(() => {
preferencesChanged();
}, [state]);
State is derived from props, so the purpose of the first useEffect is to respond to a change in props and update the state.
The purpose of the second useEffect is to make an API call when state has changed. However, this API call can result in the props of this component changing (since preferencesChanged() updates the state of a parent component).
What I really want is for the setState in the first useEffect to be done "quietly" and not to trigger the 2nd useEffect.
Is this possible? Or am I thinking about this design in completely the wrong way?
State is derived from props.
You can just do your logic inside of component (in render phase), instead of calling it in an effect:
//...
const newState = mapPropsToState(props);
// ...
and you can use useMemo if that is an expensive calculation:
const newState = useMemo(() => mapPropsToState(props), [props])
And to answer your question,
What I really want is for the setState in the first useEffect to be done "quietly" and not to trigger the 2nd useEffect.
You can store the relevant info (dependencies of 2nd useEffect) in a seperate state varable, and then use that.
Refs:
You might not need an effect (React docs)
Summing up some of nuances of useEffect

Using redux data in the useEffect hooks

I am now trying to call an API using data from the redux store.
Let say I got 2 API calls, Api A and Api B Inside the parent component I already called the API A and save the data inside the redux already.
Now I am in another component. I need to call Api B. But API B has a params, which I will get from API A. So Inside the Second component, I am using useEffect hook to call the data.
To get the params from the redux, I am using useSelector Hook.
Inside the second component, UseEffect Hook is something like this:
useEffect(() => {
let splitText = cartList?.OrderDTO?.DeliveryCountry;
let deliveryAddressId = splitText?.split(',');
if (cartList.OrderDTO?.DeliveryCountry !== '') {
dispatch(getShippingMethodById(token.access_token, deliveryAddressId));
} else {
dispatch(
getShippingMethodById(
token.access_token,
cartList.OrderDTO?.CustomerAddressId,
),
}
}, []);
So in the useEffect hook, I got the deliveryAddressId from redux. To draw in data from the redux into component, I am using useSelector hook
let cartList = useSelector((state) => state.productReducer.cartList);
The problem is that I always get undefined for cartlist when ever I tried to access it inside the useEffect hook
So the dispatch called are always getting undefined. So What can I do to make this hooks works?
You should add cartList to your dependency array, so the useEffect hook watches for updates to that piece of state. As it is written now, the useEffect only runs on the first render, where cartList is probably undefined.
React - useEffect Docs
useEffect(() => {
let splitText = cartList?.OrderDTO?.DeliveryCountry;
let deliveryAddressId = splitText?.split(',');
if (cartList.OrderDTO?.DeliveryCountry !== '') {
dispatch(getShippingMethodById(token.access_token, deliveryAddressId));
} else {
dispatch(
getShippingMethodById(
token.access_token,
cartList.OrderDTO?.CustomerAddressId,
),
}
}, [cartList]); // Add 'cartList' to your dependency array here
Solution is that you add cartList inside the dependency array.
useEffect(() => {
// All your logic inside
}, [cartList]);
I don't have info about the complete parent-child component structure, but from what I understood with the error I can explain the issue.
You are using [], for useEffect dependency, which means the callback inside useEffect will be triggered only once when the component mounts.
It is possible that when your component mounted, the API call in the parent was not complete, and you have still have undefined in store for cartList.
To check this hypothesis you can add console.log in API response and
inside the useEffect.
What else you can do?
You can not render the child component until you have data from the API call.
Why adding cartList in the dependency array fixed the issue?
By dependency array inside useEffect, your useEffect call back will be
called whenever the values in the dependency array change + on the
mount.
So, at the time of mount of the child component, useEffect's callback will trigger (cartList as undefined), then when the API call is successful and after that data is pushed in state, your child component will rerender and will retrigger the callback inside useEffect with the actual(which you got from API and pushed in store) cartList data.

How to force an update for a functional component?

I'm learning redux and want to find out how useSelector updates a component, because the component doesn't have its own state.
I understand that useSelector() subscribes the component to the store, and when the store is updated, the component also updates.
Class components have this.forceUpdate(), but functional components don't have it.
How does one force-update a functional component?
You can simply do this
Add a dummy state that you can change to reliably initiate a re-render.
const [rerender, setRerender] = useState(false);
...
//And whenever you want to re-render, you can do this
setRerender(!rerender);
And this will re-render the component, since components always re-render on state change
The react-redux package relies on the rendering engine of react/react-dom to trigger the re-render of a given component that uses the useSelector hook.
If you take a look at the source of useSelector you can notice the use of useReducer:
const [, forceRender] = useReducer((s) => s + 1, 0)
As the name (forceRender) implies, redux uses this to trigger a re-render by react.
With v8 of react-redux the implementation of this mechanism changes but still relies on react-hooks for the re-render.
If you are curious how React handles re-renders, take a look at this excellent SO answer. It provides a great entry on the implementation details of how react-hooks are associated with the calling component.
I don't repeat Ryan here, but to sum it up:
The renderer keeps a reference to the component that is currently rendered. All hooks being executed during this render (no matter how deeply nested in custom-hooks they are) ultimately belong to this component.
So, the useReducer is associated with the component within which you called useSelector.
The dispatch function of useReducer triggers a re-render of this component (React either calls the render() method of a class-component or executes the function body of a functional component).
If you are curious how react-redux determines when it should force this re-render (by utilizing useReducer), take another look at the source code of useSelector.
Redux uses the subscriber-pattern to get notified of updates to the state. If the root-state of redux is updated the following things happen:
useSelector hooks in your application re-run their selector function
This re-selected state is compared to the previously selected state (by default via === comparison). The second argument to useSelector can be a comparison function to change this behavior
If the re-selected state differs from the previously selected state, a re-render is triggered via the useReducer hook.
The subscriber pattern is very react-like but potentially helps save many re-renders. Calling several useSelector hooks is cheap when compared with re-renders.
First of all, I want to mention that you don't need to do a force update when you use useSelector hook. Rerender will happen automatically whenever the selected state value will be updated.
But if you need to force update the functional component you can use this approach.
import React, { useState } from 'react';
//create your forceUpdate hook
function useForceUpdate(){
const [value, setValue] = useState(0); // integer state
return () => setValue(value => ++value); // update the state to force render
}
function MyComponent() {
// call your hook here
const forceUpdate = useForceUpdate();
return (
<div>
{/*Clicking on the button will force to re-render like force update does */}
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
I highly recommend avoiding the use of this hack, in 99% of issues you can resolve them without force update. But in any case, it's good to know that there is such a possibility in the functional component exists too.
Maybe something like this could help you:
In a Class Component you could pass a property like the one below...
<Element onSomethingHappen={
()=>{
if(shouldComponentUpdate())
this.forceUpdate();
}}/>
In the function component you can call the updater like this one:
function FunctionComponent(props){
//When you need it you can update like this one...
props.onSomethingHappen();
// Here you are ;) let me know if this helps you
}
Continuing on other answers, to keep your code clean you can create a dummy state and then set it in your own forceUpdate function:
const [helper, setHelper] = useState(false);
function forceUpdate(){
setHelper(!helper);
}
Now you can just call forceUpdate() in the rest of your code:
<div onClick={() => forceUpdate()} />

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.

Resources