useSelector is not working, redux in react - reactjs

When I use useSelector the variable is always holding its initial state. I have the feeling it is stored in some parallel galaxy and never updated. But when I retrieve the value with const store = useStore(); store.getState()... it gives the correct value (but lacks subscribtions). When I inspect the store in redux devtools I can see all the values are recorded in the store correctly. Values are just not retrieved from the store with useSelector.
What I wanted to achieve is to have some cache for user profiles, i.e. not fetch /api/profile/25 multiple times on the same page. I don't want to think of it as "caching" and make multiple requests just keeping in mind the requests are cached and are cheap but rather thinking of it as getting profiles from the store and keeping in mind profiles are fetched when needed, I mean some lazy update.
The implementation should look like a hook, i.e.
// use pattern
const client = useProfile(userId);
// I can also put console.log here to see if the component is getting updated
let outputProfileName;
if( client.state==='pending' ) {
outputProfileName = 'loading...';
} else if( client.state==='succeeded' ) {
outputProfileName = <span>{client.data.name}</span>
} // ... etc
so I placed my code in use-profile.js, having redux-toolkit slice in profile-slice.js
profile-slice.js
import {
createSlice,
//createAsyncThunk,
} from '#reduxjs/toolkit';
const entityInitialValue = {
data: undefined,
state: 'idle',
error: null
};
export const slice = createSlice({
name: 'profile',
initialState: {entities:{}},
reducers: {
updateData: (state,action) => {
// we received data, update the data and the status to 'succeeded'
state.entities[action.payload.id] = {
...entityInitialValue,
//...state.entities[action.payload.id],
data: action.payload.data,
state: 'succeeded',
error: null
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
dispatchPendStart: (state,action) => {
// no data - indicates we started fetching
state.entities[action.payload.id] = {
...entityInitialValue,
//...state.entities[action.payload.id],
data: null,
state: 'pending',
error: null
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
dispatchError: (state,action) => {
state.entities[action.payload.id] = {
//...entityInitialValue,
...state.entities[action.payload.id],
data: null,
state: 'failed',
error: action.payload.error
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
},
extraReducers: {
}
});
export const {updateData,dispatchPendStart,dispatchError} = slice.actions;
// export const selectProfile... not used
export default slice.reducer;
use-profile.js
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector, useStore } from 'react-redux';
import {
updateData as actionUpdateData,
dispatchPendStart as actionDispatchPendStart,
dispatchError as actionDispatchError,
} from './profile-slice';
//import api...
function useProfile(userId) {
const dispatch = useDispatch();
const actionFunction = async () => {
const response = await client.get(`... api endpoint`);
return response;
};
const store = useStore();
// versionControl is a dummy variable added for testing to make sure the component is updated;
// it is updated: I tried adding console.log to my component function (where I have const client = useProfile(clientId)...)
const [versionControl,setVersionControl] = useState(0);
const updateVersion = () => setVersionControl(versionControl+1);
// TODO: useSelector not working
const updateData = newVal => { dispatch(actionUpdateData({id:userId,data:newVal})); updateVersion(); };
const dispatchPendStart = newVal => { dispatch(actionDispatchPendStart({id:userId})); updateVersion(); };
const dispatchError = newVal => { dispatch(actionDispatchError({id:userId,error:newVal})); updateVersion(); };
const [
getDataFromStoreGetter,
getLoadingStateFromStoreGetter,
getLoadingErrorFromStoreGetter,
] = [
() => (store.getState().profile.entities[userId]||{}).data,
() => (store.getState().profile.entities[userId]||{}).state,
() => (store.getState().profile.entities[userId]||{}).error,
];
const [
dataFromUseSelector,
loadingStateFromUseSelector,
loadingErrorFromUseSelector,
] = [
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].data : undefined ),
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingState : 'idle' ),
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingError : undefined ),
];
useEffect( async () => {
if( !(['pending','succeeded','failed'].includes(getLoadingStateFromStoreGetter())) ) {
// if(requestOverflowCounter>100) { // TODO: protect against infinite loop of calls
dispatchPendStart();
try {
const result = await actionFunction();
updateData(result);
} catch(e) {
dispatchError(e);
throw e;
}
}
})
return {
versionControl, // "versionControl" is an approach to force component to update;
// it is updating, I added console.log to the component function and it runs, but the values
// from useSelector are the same all the time, never updated; the problem is somewhere else; useSelector is just not working
// get data() { return getDataFromStoreGetter(); }, // TODO: useSelector not working; but I need subscribtions
// get loadingState() { return getLoadingStateFromStoreGetter(); },
// get loadingError() { return getLoadingErrorFromStoreGetter(); },
data: dataFromUseSelector,
loadingState: loadingStateFromUseSelector,
loadingError: loadingErrorFromUseSelector,
};
}
export default useProfile;
store.js
import { configureStore,combineReducers } from '#reduxjs/toolkit';
import profileReducer from '../features/profile/profile-slice';
// import other reducers
export default configureStore({
reducer: {
profile: profileReducer,
// ... other reducers
},
});
component.js - actually see the use pattern above, there's nothing interesting besides the lines posted.
So
When I export loading state (I mean last lines in use-profile.js; I can suppress last three lines and uncomment the other three). So, if I use getLoadingStateFromStoreGetter (values retrieved via store.getState()...), then some profile names are displaying names that were fetched and some are holding "loading..." and are stuck forever. It makes sense. The correct data is retrieved from redux store and we have no subscribtions.
When I export the other version, created with useSelector, I always get its initial state. I never receive any user name or the value indicating "loading".
I have read many answers on StackOverflow. Some common mistakes include:
Some are saying your component is not getting updated. It's not the case, I tested it placing console.log to the code and adding the versionControl variable (see in the code) to make sure it updates.
Some answers are saying you don't update the store with reducers correctly and it still holds the same object. It's not the case, I tried both approaches, to return a fresh new object {...state,entities:{...state.entities...etc...}} and mutating the existing proxy object - both way my reducers should provide a new object and redux should notify changes.
Sometimes multiple store instances are created and things are messed. It's definitely not the case, I have a single call to configureStore() and a single component.
Also I don't see hook rules violation in my code. I have an if statement inside the useSelector fn but the useSelector hook itself is called unconditionally.
I have no idea what other reasons are causing useSelect to simply not work. Could anyone help me understand?

Ops, as usual, very simple typo is the reason. So many hours spent. Very sorry to those who have spent time trying to look at this and thanks for your time.
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingState : 'idle' )
There should be not .loadingState but .state. That's it.

Related

react-redux state does not update when in const function in react function component

To give the shortest backstory, I needed a global state and so I moved to redux for the first time.
What I want to do is run a function every 200 or so milliseconds in a component that requires information from redux and will change the state of the component which will usually cause a re-render.
Currently, my function will run but it won't get new information from redux, the value just shows up as the original.
My questions are three: Am I approaching this correctly? Why does my component keep re-rendering? And most importantly, why does redux not give me the correct value?
I tried the following steps to debug but none of them worked:
I first thought I was not dispatching correctly or that my redux state was not changing correctly. To test this, I printed out the value of the state in my redux reducer which showed that the value was being changed
I then thought I must be getting the data wrong so I logged the value of the data right after it was initialized inside the react function component, this gave me the right value and alerted me to the fact that the component kept re-rendering.
lastly, I thought the issue had to do with the fact that I was using useEffect to make sure it only ran once, when I placed my setInterval outside of useEffect it did actually have the correct value, unfortuately the function just kept starting over and over again until it started crashing the app.
So here is some code that should give you an idea of the issue (files have been truncated and abstracted to keep the code focused but the issue presents itself anyway when used in this way):
Store.js
import { configureStore } from '#reduxjs/toolkit'
import mySlice from '../features/slice/mySlice'
export default configureStore({
reducer: {
slice: mySlice,
},
})
Next mySlice.js
import { createSlice, current } from '#reduxjs/toolkit'
export const mySlice = createSlice({
name: 'slice',
initialState: {
currentWord: 0,
},
reducers: {
incrementWord: (state) => {
console.log(state.currentWord)
state.currentWord += 1
}
},
})
export const { incrementWord } = mySlice.actions
export default mySlice.reducer
next the component (which is conditionally rendered)
MyComponent.js
import React, {useEffect } from 'react';
import { Text} from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import {incrementWord} from '../../features/slice/mySlice';
const MyComponent = () => { // naturally there is more to this function but I think this makes it simpler
const dispatch = useDispatch();
const sentences = () => {return //gets data based on redux}
const currentWord = useSelector((state) => state.game.currentWord);
const tickSentence = ()=>{ // HERE IS WHERE I CANT GET THE DATA
dispatch(incrementWord());
console.log(currentWord) // Always returns 0
}
useEffect(()=>{
setInterval(()=>{tickSentence()}, 1000);
},[])
console.log(currentWord) // returns actual value
// setInterval(()=>{tickSentence()},1000); // If I use this the function works but it keeps making new intervals leading to infinite calls
return (
<Text>{sentences[currentWord]}</Text> // NOT ACTUAL USAGE
)
}
export default MyComponent;
When left running, the log monitor in React Native Debugger shows
State : {} 1 key
slice: {} many keys
currentWord: 581
Thank you guys so much in advance and I am happy to post any information and try any configuration as I have spent many hours on this already.
Edit: copied store code incorrectly.
EDITED
To prevent multiple calls of interval -> clear the interval when component unmount
useEffect(() => {
const yourInterval = setInterval(() => {
console.log('This will run every second');
}, 1000);
return () => clearInterval(yourInterval); // This will clear the interval when component unmounts
}, []);
Wrong slide initialisation here
import { configureStore } from '#reduxjs/toolkit'
import slice from '../features/slice/mySlice'
export default configureStore({
reducer: {
slice: mySlice, // mySlice is undefined, you should pass slice instead
},
})
Solution
To get value of currentWord inside your function you can create a useEffect on currentWord, and call your function inside of it with currentWord as argument.
const yourFunction = (word) => console.log(word)
useEffect(() => {
yourFunction(currentWord)
}, [currentWord]);

Use data populated by useEffect in conditional useSWR

I'm trying to resolve this issue, and I'm almost there. I'm getting the correct data from the API, and it's updating when it should, but on initial load useSWR is hitting the API with all null data.
The data come from useContext, and are set in a useEffect hook in a parent of the component that calls useSWR.
I guess what's happening is that the since useEffect isn't called until after initial hydration, the component with useSWR is being rendered before it has data.
But if the context setter isn't wrapped in a useEffect, I get
Warning: Cannot update a component (`ContestProvider`) while rendering a different component (`PageLandingPage`). To locate the bad setState() call inside `PageLandingPage`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
and it's stuck in an infinite loop.
I could probably stop this by putting some checks in the fetcher, but that seems like a hack to me. The useSWR documentation addresses the case of fetching data server side and making it available to multiple components right in the Getting Started section, but what's the correct way to get data from the client that needs to be used in multiple components, including ones that want to fetch data from the server based on the client data?
EDIT: Since originally asking the question, I've discovered conditional fetching, and the third option there seems nearly a perfect fit, but I'm using a complex key to a custom fetcher, and the data for the key aren't coming from another useSWR call, as in the example — they're coming from the useContext which has the unfortunate difference that, unlike the example, the data are null instead of undefined, so it won't throw.
How can I use this conditionality with data coming in from the useContext?
Here's the app hierarchy:
<MyApp>
<ContestEntryPage>
<ContestProvider> // context provider
<PageLandingPage> // sets the context
<Section>
<GridColumn>
<DatoContent>
<ContestPoints> // calls useSWR with data from the context
Here's the useSWR call:
// /components/ContestPoints.js
const fetcher = async ({pageId, contestId, clientId}) => {
const res = await fetch(`/api/getpoints?pageId=${pageId}&clientId=${clientId}&contestId=${contestId}`);
if (!res.ok) {
const error = new Error('A problem occured getting contest points');
error.info = await res.json();
error.status = res.status;
throw error;
}
return res.json();
}
const ContestPoints = () => {
const { contestState } = useContest();
// XXX should be conditional on the `contestState` parameters
const { data: points, error } = useSWR({
pageId: contestState.pageId,
contestId: contestState.contestId,
clientId: contestState.clientId
}, fetcher);
if (error) {
logger.warn(error, `Problem getting contest points: ${error.status}: ${error.info}`);
}
return (
<p>{points?.points || 'Loading...'}</p>
)
}
export default ContestPoints
It seems like finding a way to make that do the conditional fetching is likely best, but in case it's more elegant to leave the useSWR call as is, and address this farther up the chain, here are the other relevant pieces of code.
The context is being set based on information in localStorage:
// /components/PageLandingPage.js
import { useContest } from '../utils/context/contest';
const PageLandingPage = ({ data }) => {
const { dispatchContest } = useContest(); // wrapper around useContext which uses useReducer
useEffect(() => {
// Don't waste the time if we're not a contest page
if (!data?.contestPage?.id) return;
const storedCodes = getItem('myRefCodes', 'local'); //utility function to retrieve from local storage
const refCodes = storedCodes ? JSON.parse(storedCodes)?.refCodes : [];
const registration = refCodes
.map((code) => {
const [ contestId, clientId ] = hashids.decode(code);
return {
contestId: contestId,
clientId: clientId
}
})
.find((reg) => reg.contestId && reg.clientId)
dispatchContest({
payload: {
pageId: data.contestPage.id,
contestId: registration.contestId,
clientId: registration.clientId,
registrationUrl: landingPage?.registrationPage?.slug || ''
},
type: 'update'
})
}, [data, dispatchContest])
...
And the context wrapper is initialising with null state:
const initialState = {
contestId: null,
clientId: null
};
const ContestContext = createContext(initialState);
function ContestProvider({ children }) {
const [contestState, dispatchContest] = useReducer((contestState, action) => {
return {
...contestState,
...action.payload
}
}, initialState);
return (
<ContestContext.Provider value={{ contestState, dispatchContest }}>
{children}
</ContestContext.Provider>
);
}
function useContest() {
const context = useContext(ContestContext);
if (context === undefined) {
throw new Error('useContest was used outside of its provider');
}
return context;
}
export { ContestProvider, useContest }
I'm not sure it's the best solution, but I ended up resolving this by using the first example in the documentation, using null and a new field in the context:
const { data: points, error } = useSWR(contestState?.isSet ? {
pageId: contestState.pageId,
contestId: contestState.contestId,
clientId: contestState.clientId
} : null, fetcher);
and the contestState.isSet gets set in the context:
const initialState = {
isSet: false,
contestId: null,
clientId: null
};
and update it when all the other fields get set:
dispatchContest({
payload: {
isSet: true,
pageId: data.contestPage.id,
contestId: registration.contestId,
clientId: registration.clientId,
registrationUrl: landingPage?.registrationPage?.slug || ''
},
type: 'update'
})

React Testing Library - how to correctly test that my provider is being populated with data and the child components are displaying properly?

I have a pretty simple use case - I have a global app context where I'm trying to store data fetched from an endpoint. My goal is to load this data into the context on app load and I'm going about it using the useReducer hook. I settled on the solution of calling an action getIssuerDetails() that dispatches various states throughout the method and invokes the issuerApi service to actually call the API (it's a simple Axios GET wrapper). This action is called from a useEffect within the Provider and is called on mount as shown below.
I'm having some trouble wrapping my head around how to properly test that 1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider. Am I approaching this data fetching portion correctly? I can either make the actual API call within my App component on mount and then dispatch actions to update the global state OR I keep my solution of fetching my data from within the useEffect of the provider.
I know I'm not supposed to be testing implementation details but I'm having a hard time separating out what data/methods I should mock and which ones I should allow to execute on their own. Any help would be greatly appreciated.
AppContext.tsx
import { createContext, FC, useEffect, useContext, useReducer, useRef } from 'react';
import { getIssuerDetails } from './issuer/actions';
import { appStateReducer } from './global/reducer';
import { combineReducers } from '#utils/utils';
import { GlobalAppStateType } from './global/types';
/**
* Our initial global app state. It just stores a bunch
* of defaults before the data is populated.
*/
export const defaultInitialState = {
issuerDetails: {
loading: false,
error: null,
data: {
issuerId: -1,
issuerName: '',
ipoDate: '',
ticker: '',
},
},
};
export type AppStateContextProps = {
state: GlobalAppStateType;
};
export type AppDispatchContextProps = {
dispatch: React.Dispatch<any>;
};
export const AppStateContext = createContext<AppStateContextProps>({
state: defaultInitialState,
});
export const AppDispatchContext = createContext<AppDispatchContextProps>({
dispatch: () => null,
});
/**
*
* #param
* #returns
*/
export const mainReducer = combineReducers({
appState: appStateReducer,
});
export type AppProviderProps = {
mockInitialState?: GlobalAppStateType;
mockDispatch?: React.Dispatch<any>;
};
/**
* Our main application provider that wraps our whole app
* #param mockInitialState - mainly used when testing if we want to alter the data stored in our
* context initially
* #param children - The child components that will gain access to the app state and dispatch values
*/
export const AppProvider: FC<AppProviderProps> = ({ mockInitialState, mockDispatch, children }) => {
const [state, dispatch] = useReducer(mainReducer, mockInitialState ? mockInitialState : defaultInitialState);
const nState = mockInitialState ? mockInitialState : state;
const nDispatch = mockDispatch ? mockDispatch : dispatch;
// Ref that acts as a flag to aid in cleaning up our async data calls
const isCanceled = useRef(false);
useEffect(() => {
async function fetchData() {
// Await the API request to get issuer details
if (!isCanceled.current) {
await getIssuerDetails(nDispatch);
}
}
fetchData();
return () => {
isCanceled.current = true;
};
}, [nDispatch]);
return (
<AppStateContext.Provider value={{ state: nState }}>
<AppDispatchContext.Provider value={{ dispatch: nDispatch }}>{children}</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
};
/**
* Custom hook that gives us access to the global
* app state
*/
export const useAppState = () => {
const appStateContext = useContext(AppStateContext);
if (appStateContext === undefined) {
throw new Error('useAppState must be used within a AppProvider');
}
return appStateContext;
};
/**
* Custom hook that gives us access to the global
* app dispatch method to be able to update our global state
*/
export const useAppDispatch = () => {
const appDispatchContext = useContext(AppDispatchContext);
if (appDispatchContext === undefined) {
throw new Error('useAppDispatch must be used within a AppProvider');
}
return appDispatchContext;
};
AppReducer.ts
Note: Code still needs to be cleaned up here but it's functioning at the moment.
import * as T from '#context/global/types';
export const appStateReducer = (state: T.GlobalAppStateType, action: T.GLOBAL_ACTION_TYPES) => {
let stateCopy;
switch (action.type) {
case T.REQUEST_ISSUER_DETAILS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = true;
return stateCopy;
case T.GET_ISSUER_DETAILS_SUCCESS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.data = action.payload;
return stateCopy;
case T.GET_ISSUER_DETAILS_FAILURE:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.error = action.payload;
return stateCopy;
default:
return state;
}
};
getIssuerDetails()
export const getIssuerDetails = async (dispatch: React.Dispatch<any>) => {
dispatch({ type: GlobalState.REQUEST_ISSUER_DETAILS, payload: null });
try {
// Fetch the issuer details
const response = await issuerApi.getIssuerDetails(TEST_ISSUER_ID);
if (response) {
/***************************************************************
* React Testing Library gives me an error on the line below:
* An update to AppProvider inside a test was not wrapped in act(...)
***************************************************************/
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_SUCCESS, payload: response });
return response;
}
// No response
dispatch({
type: GlobalState.GET_ISSUER_DETAILS_FAILURE,
error: { message: 'Could not fetch issuer details.' },
});
} catch (error) {
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_FAILURE, error });
}
};
dashboard.test.tsx
import { render, screen, cleanup, act } from '#testing-library/react';
import { AppProvider, AppStateContext } from '#context/appContext';
import { GlobalAppStateType } from '#context/global/types';
afterEach(() => {
cleanup();
jest.clearAllMocks();
});
describe('Dashboard page', () => {
it('should render the page correctly', async () => {
act(() => {
render(
<AppProvider>
<Dashboard />
</AppProvider>
);
});
expect(await screen.findByRole('heading', { level: 1 })).toHaveTextContent('Stock Transfer');
});
});
I won't dive into the code specifically since there is too much you want to test all at once.
From what I could gather, you are trying to do an Integration Test and not a Unitary Test anymore. No problem there, you just need to define where you want to draw the line. For me, it's pretty clear that the line lies in the issuerApi.getIssuerDetails call, from which you could easily mock to manipulate the data how you want.
1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider.
Well, I would advise you to make a simple mock component that uses the hook and displays the data after fetching. You could make a simple assertion for that, no need for an actual component (<Dashboard />).
Am I approaching this data fetching portion correctly?
It all depends on how you want to structure it but ideally the AppProvider should be thin and lay those data fetching and treatments inside a service just for that. This would provide a better way to unit test the components and smoother code maintenance.

React / Redux: why useEffect is called too many times?

First of all, I'm sorry if the first question is unfamiliar and difficult to understand
Purpose:
Get data from firestore and fetch again when list of store is updated.
add info.
I want to get data from a firestore and redraw the screen when the list of stores is updated.
This is a general list update.
problem:
useEffect wants to execute fetch when the value of stores is updated, but useEffect is always called and fetch request to firestore does not stop.
The code is below.
Test.jsx
...
import { useDispatch, useSelector } from "react-redux";
import { fetchStores } from "../../reducks/stores/operations";
const storesRef = db.collection('stores');
const Test = () => {
const dispatch = useDispatch();
const selector = useSelector(state=>state);
// stores set initialState
// stores: {
// name: "",
// list: [],
// }
const stores = selector.stores.list;
useEffect(()=>{
dispatch(fetchStores())
},[stores])
}
...
reducks/stoers/operations.js
export const fetchStores = () => {
return async (dispatch) => {
storesRef.where().get()
.then(snapshots => {
const storeList = []
snapshots.forEach(snapshot => {
const store = snapshot.data();
store['id'] = snapshot.id;
storeList.push(store)
})
dispatch(fetchStoresAction(storeList))
})
}
}
*fetchStoresAction saves an array in the store list.
thank you
You should have an empty dependency array in useEffect so the fetch request only executed once when the components gets mounted:
useEffect(() => {
dispatch(fetchStores())
}, [])
If you would like to fetch stores when new store is created or updated, you should do it in a function where you can access the update or create fetch request's result. If the result is successful (successful update on database), then you can fetch your stores again.

Update single element of the array in redux state

I have a redux state that contains an array of objects, for each of these object I call an api to get more data
objects.forEach((obj, index) => {
let newObj = { ...obj };
service.getMoreData()
.then(result => {
newObj.data = result;
let newObjects = [...this.props.objectsList] ;
let index = newObjects.findIndex(el => el.id === newObj.id);
if (index != -1) {
newObjects[index] = newObj;
this.props.updateMyState({ objectsList: newObjects });
}
})
When I get two very close responses the state is not updated correctly, I lose the data of the first response.
What is the right way to update a single element of the array? Thanks!
So since i don't know what service is and there isn't that much here to go off, here is what I would do from my understanding of what it looks like your doing:
So first let's set up a reducer to handle the part of redux state that you want to modify:
// going to give the reducer a default state
// array just because I don't know
// the full use case
// you have an id in your example so this is the best I can do :(
const defaultState = [{ id: 123456 }, { id: 123457 }];
const someReducer = (state=defaultState, action) => {
switch (action.type) {
// this is the main thing we're gonna use
case 'UPDATE_REDUX_ARRAY':
return [
...action.data
]
// return a default state == the state argument
default:
return [
...state
]
}
}
export default someReducer;
Next you should set up some actions for the reducer, this is optional and you can do it all inline in your component but I'd personally do it this way:
// pass data to the reducer using an action
const updateReduxArray = data => {
return {
type: 'UPDATE_REDUX_ARRAY',
data: data
}
}
// export like this because there might
// be more actions to add later
export {
updateReduxArray
}
Then use the reducer and action with React to update / render or whatever else you want
import { useState } from 'react';
import { updateReduxArray } from 'path_to_actions_file';
import { useEffect } from 'react';
import { axios } from 'axios';
import { useSelector, useDispatch } from 'react-redux';
const SomeComponent = () => {
// set up redux dispatch
const dispatch = useDispatch();
// get your redux state
const reduxArray = useSelector(state => state.reduxArray) // your gonna have to name this however your's is named
// somewhere to store your objects (state)
const [arrayOfObjects, updateArrayOfObjects] = useState([]);
// function to get data from your API
const getData = async () => {
// I'm using axios for HTTP requests as its pretty
// easy to use
// if you use map you can just return the value of all API calls at once
const updatedData = await Promise.all(reduxArray.map(async (object, index) => {
// make the api call
const response = axios.get(`https://some_api_endpoint/${object.id}`)
.then(r => r.data)
// return the original object with the addition of the new data
return {
...response,
...object
}
}))
// once all API calls are done update the state
// you could just update redux here but this is
// a clean way of doing it incase you wanna update
// the redux state more than once
// costs more memory to do this though
updateArrayOfObjects(updatedData)
}
// basicity the same as component did mount
// if you're using classes
useEffect(() => {
// get some data from the api
getData()
}, [ ])
// every time arrayOfObjects is updated
// also update redux
useEffect(() => {
// dispatch your action to the reducer
dispatch(updateReduxArray(arrayOfObjects))
}, [arrayOfObjects])
// render something to the page??
return (
<div>
{ reduxArray.length > 0
? reduxArray.map(object => <p>I am { object.id }</p>)
: <p>nothing to see here</p>
}
</div>
)
}
export default SomeComponent;
You could also do this so that you only update one object in redux at a time but even then you'd still be better off just passing the whole array to redux so I'd do the math on the component side rather than the reducer .
Note that in the component I used react state and useEffect. You might not need to do this, you could just handle it all in one place when the component mounts but we're using React so I just showcased it incase you want to use it somewhere else :)
Also lastly I'm using react-redux here so if you don't have that set up (you should do) please go away and do that first, adding your Provider to the root component. There are plenty of guides on this.

Resources