How can I memoize a variable derived from redux useSelector? - reactjs

How can I memoize my rawTranscript variable so it doesn't trigger the useEffect below which subsequently triggers the expensive transcriptParser function? I've been trying a lot of different approaches, but the fact that I am using a redux-hook (useAppSelector) to capture the data from the store means I cannot use an empty dependency useEffect for the initial mount (hooks can't be inside of useEffect). I also can't seem to wrap the useAppSelector with a useMemo either for the same reason.
Any thought's on how I can memoize the rawTranscript variable so it doesn't re-trigger the useEffect?
error when using the redux-hook inside useMemo, useEffect, useCallback:
React Hook "useAppSelector" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.
component
const TranscriptCardController = (): JSX.Element => {
const dispatch = useAppDispatch();
// how to memoize rawTranscript?
const rawTranscript = useAppSelector(selectRawTranscript);
const parsedTranscript = useAppSelector(selectParsedTranscript);
useEffect(() => {
const parsedResult = transcriptParser(rawTranscript, undefined, 0.9);
dispatch(updateParsedTranscript(parsedResult));
}, [dispatch, rawTranscript]);
// ...
};
selector
export const selectRawTranscript = createSelector(
(state: RootState) => state.transcript.rawTranscript,
(rawTranscript): RawTranscript => rawTranscript
);

There is no issue here if your selectRawTranscript function is purely selecting a value from the store, like state => state.transcript.raw. Your effect will only run when the value of rawTranscript changes -- as it should.
If your selectRawTranscript function returns a new object every time (like it if it involves array mapping, etc.) then this is a problem that you can address either in the selector itself or in the component.
Memoized Selectors
The best place to fix this is by using createSelector to create a memoized selector. For example:
import {createSelector} from '#reduxjs/toolkit';
export const selectRawTranscript = createSelector(
(state: RootState) => state.data.someRawValue,
(rawValue) => rawValue.map(entry => entry.data)
);
The second part of the selector is the "combiner" and it will only re-run when the value selected in the first part changes. So you get a consistent object reference.
Equality Comparisons
If you want to fix this in the component, the way to do that is by including a second argument on useAppSelector (which I'm assuming is just a typed version of useSelector).
This second argument allows you to specify a custom equality function so that you have more control over when the selected data is considered to be "changed". It's common to use a shallow equality comparison, so this is actually included in the react-redux package.
import { shallowEqual } from 'react-redux';
import { useAppSelector } from ...
const TranscriptCardController = (): JSX.Element => {
const rawTranscript = useAppSelector(selectRawTranscript, shallowEqual);
...
Note: it's impossible for me to know whether or not you really do have a problem with undesirable changes in rawTranscript because you haven't included your selector function. You might be overthinking this and it might be a non-issue.

Create a standalone useCallback where your dispatch will run on every store update but useEffect will only execute when the callback method is executed.
const TranscriptCardController = (): JSX.Element => {
const dispatch = useAppDispatch();
// how to memoize rawTranscript?
const rawTranscript = useAppSelector(selectRawTranscript);
const parsedTranscript = useAppSelector(selectParsedTranscript);
const callback = useCallback(() => {
const parsedResult = transcriptParser(rawTranscript, undefined, 0.9);
dispatch(updateParsedTranscript(parsedResult));
}, [rawTranscript])
useEffect(() => {
const unsubscribe = callback()
return unsubscribe
}, [callback]);
// ...
};

Related

Using the Context API as a way of mimicking useSelector and useDispatch with redux v5

I'm working on a React project where I'm constrained to using React Redux v5, which doesn't include useDispatch and useSelector.
Nonetheless I really would like to have these hooks (or something like them) available in my app.
Therefore, I've created a wrapper component at the top level of the app which I connect using redux's connect function.
My mapStateToProps and mapDispatchToProps then just look like this:
const mapDispatchToProps = (dispatch: DispatchType) => {
return {
dispatch,
};
};
const mapStateToProps = (state: StateType) => {
return {
state,
};
};
export default connect(mapStateToProps, mapDispatchToProps)(MainLayout);
In my wrapper component, I then pass the dispatch and the state into the value:
<DispatchContext.Provider value={{ state, dispatch }}>
{children}
</DispatchContext.Provider>
Finally, I have a hook that looks like this:
const useSelectAndDispatch = () => {
const context = useContext(DispatchContext);
if (context === null) {
throw new Error("Please use useDispatch within DispatchContext");
}
const { state, dispatch } = context;
function select(selector) {
return selector(state);
}
return { dispatch, select };
};
I then use dispatch and selector in my components via useSelectAndDispatch.
I was wondering if this is an appropriate way to go about this issue, and whether I can expect any performance problems. I am using reselect, and have a good understanding of memoization. I'm just looking for opinions, since I've heard that the redux team held off implementing useDispatch and useSelector for a long time because of performance issues.
Many thanks for any opinions!
This will cause significant peformance problems. Your mapStateToProps is using the entire state object, so every time anything changes in the state, the provider must rerender. And since the provider rerendered with a new value, so too must every component that consumes the context. In short, you will be forcing most of your app to rerender anytime anything changes.
Instead of using mapStateToProps and mapDispatchToProps, i would go back to the actual store object, and build your hooks from that. Somewhere in your app is presumably a line of code that says const store = createStore(/* some options */).
Using that store variable, you can then make some hooks. If i can assume that there's only one store in your app, then the dispatch hook is trivial:
import { store } from 'wherever the store is created'
export const useMyDispatch = () => {
return store.dispatch;
}
And the selector one would be something like this. It uses .subscribe to be notified when something in the store changes, and then it uses the selector to pluck out the part of the state that you care about. If nothing changed, then the old state and new state will pass an === check, and react skips rendering. If it has changed though, the component renders (only the component that called useMySelect plus its children, not the entire app)
export const useMySelector = (selector) => {
const [value, setValue] = useState(() => {
return selector(store.getState());
});
useEffect(() => {
const unsubscribe = store.subscribe(() => {
const newValue = selector(store.getState());
setValue(newValue);
});
return unsubscribe;
}, []);
return value;
}

useEffect on infinite loop using async fetch function

I am trying to understand why the following useEffect is running in an infinite loop. I made the fetchSchedule helper function to call the getSchedule service (using Axios to query the API endpoint). Reason I did not define this function inside the useEffect hook is because I would like to alternatively also call it whenever the onStatus function is invoked (which toggles a Boolean PUT request on a separate endpoint).
The eslinter is requiring fetchSchedule be added to the array of dependencies, which seems to be triggering the infinite loop.
The way it should work is fetching the data from the database on first render, and then only each time either the value prop is updated or the onStatus button is toggled.
So far my research seems to point that this may have something to do with the way useEffect behaves with async functions and closures. I’m still trying to understand Hooks and evidently there’s something I’m not getting in my code…
import React, { useEffect, useCallback } from 'react';
import useStateRef from 'react-usestateref';
import { NavLink } from 'react-router-dom';
import { getSchedule, updateStatus } from '../../services/scheduleService';
import Status from './status';
// import Pagination from './pagination';
const List = ({ value }) => {
// eslint-disable-next-line
const [schedule, setSchedule, ref] = useStateRef([]);
// const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
}, [value, setSchedule]);
const onStatus = (id) => {
updateStatus(id);
fetchSchedule();
console.log('fetch', ref.current[0].completed);
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
return (...)
Update March 2021
After working with the repo owner for react-usestateref, the package now functions as originally intended and is safe to use as a replacement for useState as of version 1.0.5. The current implementation looks like this:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(state);
var dispatch = React.useCallback(function(val) {
ref.current = typeof val === "function" ?
val(ref.current) : val;
setState(ref.current);
}, []);
return [state, dispatch, ref];
};
You would be fine if it weren't for this react-usestateref import.
The hook returns a plain anonymous function for setting state which means that it will be recreated on every render - you cannot usefully include it in any dependency array as that too will be updated on every render. However, since the function is being returned from an unknown custom hook (and regardless, ESLint would correctly identify that it is not a proper setter function) you'll get warnings when you don't.
The 'problem' which it tries to solve is also going to introduce bad practice into your code - it's a pretty way to avoid properly handling dependencies which are there to make your code safer.
If you go back to a standard state hook I believe this code will work fine. Instead of trying to get a ref of the state in onStatus, make it async as well and return the data from fetchSchedule as well as setting it.
const [schedule, setSchedule] = useState([]);
const fetchSchedule = useCallback(async () => {
const { data } = await getSchedule(value);
setSchedule(data);
return data;
}, [value]);
const onStatus = async (id) => {
updateStatus(id);
const data = await fetchSchedule();
};
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
Alternatively, although again I wouldn't really recommend using this, we could actually write a safe version of the useStateRef hook instead:
function useStateRef(defaultValue) {
var [state, setState] = React.useState(defaultValue);
var ref = React.useRef(defaultValue);
ref.current = state;
return [state, setState, ref];
}
A state setter function is always referentially identical throughout the lifespan of a component so this can be included in a dependency array without causing the effect/callback to be recreated.

useEffect re-renders too many times

I have this component, that needs to fetch data, set it to state and then pass it to the children.
Some of the data also needs to be set in context.
My problem is that using useEffect, once called the API, it will re-render for each setvalue() function I need to execute.
I have tried passing to useEffect an empty [] array, still getting the same number of re-renders, due to the fact that the state is changing.
At the moment the array is containg the set...functions to prevent eslint to throw warnings.
Is there a better way to avoid this many re-renders ?
const Home = (props) => {
console.log("TCL: Home -> props", props);
const classes = useStyles();
const [value, setValue] = React.useState(0);
//CONTEXT
const { listSavedJobs, setListSavedJobs, setIsFullView} = useContext(HomeContext);
const {
setUserName,
setUserLastName,
setUserEmail,
setAvatarProfile,
} = useContext(UserContext);
// STATE
const [searchSettings, setSearchSettings] = useState([]);
const [oppData, setOppData] = useState([]);
const handleChange = (event, newValue) => {
setValue(newValue);
};
const handleChangeIndex = index => {
setValue(index);
};
//API CALLS
useEffect(() => {
const triggerAPI = async () => {
setIsFullView(false);
const oppResponse = await API.getOpportunity();
if(oppResponse){
setOppData(oppResponse.response);
}
const profileResponse = await API.getUserProfile();
if(profileResponse){
setUserName(profileResponse.response.first_name);
setUserLastName(profileResponse.response.last_name);
setUserEmail(profileResponse.response.emailId);
}
const profileExtData = await API.getUserProfileExt();
if(profileExtData){
setAvatarProfile(profileExtData.response.avatar);
setListSavedJobs(profileExtData.response.savedJobs);
setSearchSettings(profileExtData.response.preferredIndustry);
}
};
triggerAPI();
}, [
setOppData,
setUserName,
setUserLastName,
setUserEmail,
setAvatarProfile,
setListSavedJobs,
setIsFullView,
]);
...```
Pass just an empty array to second parameter of useEffect.
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
Source
Edit: Try this to avoid rerenders. Use with caution
Only Run on Mount and Unmount
You can pass the special value of empty array [] as a way of saying “only run on mount and unmount”. So if we changed our component above to call useEffect like this:
useEffect(() => {
console.log('mounted');
return () => console.log('unmounting...');
}, [])
Then it will print “mounted” after the initial render, remain silent throughout its life, and print “unmounting…” on its way out.
Prevent useEffect From Running Every Render
If you want your effects to run less often, you can provide a second argument – an array of values. Think of them as the dependencies for that effect. If one of the dependencies has changed since the last time, the effect will run again. (It will also still run after the initial render)
const [value, setValue] = useState('initial');
useEffect(() => {
// This effect uses the `value` variable,
// so it "depends on" `value`.
console.log(value);
}, [value])
For more clarification useEffect
If you are using React 18, this won't be a problem anymore as the new auto batching feature: https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching
If you are using an old version, can refer to this solution: https://statics.teams.cdn.office.net/evergreen-assets/safelinks/1/atp-safelinks.html

React get state from Redux store within useEffect

What is the correct way to get state from the Redux store within the useEffect hook?
useEffect(() => {
const user = useSelector(state => state.user);
});
I am attempting to get the current state within useEffect but I cannot use the useSelector call because this results in an error stating:
Invariant Violation: Hooks can only be called inside the body of a function component.
I think I understand why as it breaks one of the primary rules of hooks.
From reviewing the example on the Redux docs they seem to use a selectors.js file to gather the current state but this reference the mapStateToProps which I understood was no longer necessary.
Do I need to create some kind of "getter" function which should be called within the useEffect hook?
Don't forget to add user as a dependency to useEffect otherwise your effect won't get updated value.
const user = useSelector(state => state.user);
useEffect(() => {
// do stuff
}, [user]);
You can place useSelector at the top of your component along with the other hooks:
const MyComponent = () => {
...
const user = useSelector(state => state.user);
...
}
Then you can access user inside your useEffects.
I found using two useEffects to works for me, and have useState to update the user (or in this case, currUser).
const user = useSelector(state=>state.user);
const [currUser, setCurrUser] = useState(user);
useEffect(()=>{
dispatch(loadUser());
}, [dispatch]);
useEffect(()=>{
setCurrUser(user);
}, [user]);
You have to use currUser to display and manipulate that object.
You have two choices.
1 - If you only need the value from store once or 'n' time your useEffect is called and don't want to listen for any changes that may occur to user state from redux then use this approach
//import the main store file from where you've used createStore()
import {store} from './store' // this will give you access to redux store
export default function MyComponent(){
useEffect(() =>{
const user = store.getState().user;
//...
},[])
}
2 - If you want to listen to the changes that may occur to user state then the recommended answer is the way to go about
const MyComponent = () => {
//...
const user = useSelector(state => state.user);
useEffect(() => {
//...
},[])
//...
}
const tournamentinfofromstore=useSelector(state=>state.tournamentinfo)
useEffect(() => {
console.log(tournamentinfofromstore)
}, [tournamentinfofromstore])
So the problem is that if you change the state inside the useEffect that causes a rerender and then again the useEffect gets called "&&" if that component is passing data to another component will result in infinite loops.and because you are also storing that data in the child component's state will result in rerendering and the result will be infinite loop.!!
Although it is not recommended, you can use store directly in your component, even in the useEffect.
First, you have to export store from where it is created.
import invoiceReducer from './slices/invoiceSlice';
import authReducer from './slices/authSlice';
export const store = configureStore({
reducer: {
invoices: invoicesReducer,
auth: authReducer,
},
});
Then you can import it to a React Component, or even to a function, and use it.
import React, { useEffect } from 'react';
import { store } from './store';
const MyComponent = () => {
useEffect(()=> {
const invoiceList = store.getState().invoices
console.log(invoiceList)
}, [])
return (
<div>
<h1>Hello World!</h1>
</div>
)
}
export default MyComponent
You can study the API for Store in here.
You can also see why this approach is not recommended in
here.
Or, if you are interested in using redux store outside a react component, take a look at this blog post.
To add on top of #Motomoto's reply. Sometimes you depend on store to be loaded before useEffect. In this case you can simply return in if the state is undefined. useEffect will rerender once the store is loaded
const user = useSelector(state => state.user);
useEffect(() => {
if(user === undefined){
return}else{
// do stuff
}}, [user]);
I'm having the same issue, The problem to the useSelector is that we cant call it into the hook, so I can't be able to update with the action properly. so I used the useSelector variable as a dependency to the useEffect and it solved my problem.
const finalImgData_to_be_assigned = useSelector((state) => state.userSelectedImg);
useEffect(()=>{
console.log('final data to be ready to assign tags : ', finalImgData_to_be_assigned.data);
}, [finalImgData_to_be_assigned ])

useEffect props callback function causing infinite loop

I have a problem very similar to this - How do I fix missing dependency in React Hook useEffect.
There is one key difference - I am passing a fetch function to a child component to be called from useEffect, so I can't simply move the function into the body of the effect. The fetch function is re-created every render and causes an infinite loop. I have other local component state that I want to cause the effect to fire.
I basically have a Container Component and a Presentational component. MyPage is the parent of MyGrid and sets up all the redux state:
const MyPage = () => {
const dispatch = useDispatch();
const items= useSelector(selectors.getItems);
const fetching = useSelector(selectors.getFetching);
const fetchItems = opts => dispatch(actions.fetchItems(opts));
return (
<>
{fetching && <div>Loading...</div>}
<h1>Items</h1>
<MyGrid
items={items}
fetchItems={fetchItems}
fetching={fetching}
/>
</>
);
}
const MyGrid = ({ fetchItems, items, fetching }) => {
const [skip, setSkip] = useState(0);
const take = 100;
const [sorts, setSorts] = useState([]);
// when query opts change, get data
useEffect(() => {
const options = { skip, take };
const sortString = getSortString(sorts);
if (sortString) options['sort'] = sortString;
fetchItems(options);
}, [fetchItems, skip, sorts]);
In "MyGrid" "skip" and "sorts" can change, and should make the effect fire.
"fetchItems" is re-created everytime and causes an infinite loop. This
is my problem.
Now, the eslint react-hooks/exhaustive-deps rule is making me put fetchItems in the dependency list. I have prettier setup to autofix on save which makes it worse.
I know the Container/Presentational pattern is out of style with hooks, but it works good for my situation - I may allow swapping out MyGrid for MyList dynamically and don't want to repeat all the redux stuff in each child component.
I tried to useCallback and useMemo, but eslint just makes me put all the same dependencies in it's dependency array parameter.
Is there a way other than disabling the eslint rule
// eslint-disable-next-line react-hooks/exhaustive-deps
to make this work?
There are two ways, you can make it work.
Firstly, using useCallback for fetchItem like
const fetchItems = useCallback(opts => dispatch(actions.fetchItems(opts)), [dispatch, actions]);
Secondly using dispatch directly in child component
const dispatch = useDispatch();
useEffect(() => {
const options = { skip, take };
const sortString = getSortString(sorts);
if (sortString) options['sort'] = sortString;
dispatch(actions.fetchItems(options));
}, [dispatch, actions, skip, sorts]);

Resources