Getting infinite loop when using React Context Dispatch in useEffect - javascript

I'm working on a project and created a context that is supposed to store my projects data. I included the context dispatch inside of a useEffect in a component which is supposed to pass the data object to the context but I am running into an issue where I am an infinite loop. I completely simplified the structure and I still can't figure out what my problem is. I pasted the code below. Does anyone know what I could be doing wrong?
// DataContext.js
import React, { useReducer } from "react";
export const DataContext = React.createContext();
const dataContextInitialState = { test: 1 };
const dataContextReducer = (state, action) => {
switch (action.type) {
case "update":
console.log(state);
return {
...state,
action.value,
};
default:
return dataContextInitialState;
}
};
export const DataContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(
dataContextReducer,
dataContextInitialState
);
return (
<DataContext.Provider
value={{
state,
dispatch,
}}
>
{children}
</DataContext.Provider>
);
};
// Component that is accessing context
.
.
.
useEffect(() => {
dataContext.dispatch({
type: "update",
});
}, [dataContext]);
.
.
.

I think this happens because the useEffect gets called during the first render, it executes dispatch that calls your reducer which returns a new object { dataContextInitialState }.
The new obect is passed down to your component, useEffect checks if the dataContext object is the same as in the previous render, but it is different because it's a new object so it re-executes the useEffect and you have the loop.
A possible solution
From my understanding with this piece of code
const dataContextInitialState = { test: 1 };
case "update":
return {
dataContextInitialState,
};
your state becomes this:
{
dataContextInitialState: {
test: 1
}
}
I guess that what you wanted was to have a state which is an object with a key names test, you can try modifying your code like this:
case "update":
return dataContextInitialState;

Related

context api - useEffect doesn't fire on first render - react native

The useEffect doesn't fire on first render, but when I save the file (ctrl+s), the state updates and the results can be seen.
What I want to do is, when I'm in GameScreen, I tap on an ICON which takes me to WalletScreen, from there I can select some items/gifts (attachedGifts - in context) and after finalising I go back to previous screen i.e. GameScreen with gifts attached (attachedGifts!==null), now again when I tap ICON and go to WalletScreen it should show me the gifts that were attached so that I could un-attach them or update selection (this is being done in the useEffect below in WalletScreen), but the issue is, although my attachedGifts state is updating, the useEffect in WalletScreen does not fire immediately when navigated, when I hit ctrl+s to save the file, then I can see my selected/attached gifts in WalletScreen.
code:
const Main = () => {
return (
<GiftsProvider>
<Stack.Screen name='WalletScreen' component={WalletScreen} />
<Stack.Screen name='GameScreen' component={GameScreen} />
</GiftsProvider>
)
};
const GameScreen = () => {
const { attachedGifts } = useGifts(); //coming from context - GiftsProvider
console.log('attached gifts: ', attachedGifts);
return ...
};
const WalletScreen = () => {
const { attachedGifts } = useGifts();
useEffect(() => { // does not fire on initial render, after saving the file, then it works.
if (attachedGifts !== null) {
let selectedIndex = -1
let filteredArray = data.map(val => {
if (val.id === attachedGifts.id) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
setData(filteredArray);
}
}, [attachedGifts]);
const attachGiftsToContext = (obj) => {
dispatch(SET_GIFTS(obj));
showToast('Gifts attached successfully!');
navigation?.goBack(); // goes back to GameScreen
}
return (
// somewhere in between
<TouchableOpacity onPress={attachGiftsToContext}>ATTACH</TouchableOpacity>
)
};
context:
import React, { createContext, useContext, useMemo, useReducer } from 'react';
const GiftsReducer = (state: Object | null, action) => {
switch (action.type) {
case 'SET_GIFTS':
return action.payload;
default:
return state;
}
};
const GiftContext = createContext({});
export const GiftsProvider = ({ children }) => {
const initialGiftState: Object | null = null;
const [attachedGifts, dispatch] = useReducer(
GiftsReducer,
initialGiftState,
);
const memoedValue = useMemo(
() => ({
attachedGifts,
dispatch,
}),
[attachedGifts],
);
return (
<GiftContext.Provider value={memoedValue}>
{children}
</GiftContext.Provider>
);
};
export default function () {
return useContext(GiftContext);
}
Output of console.log in GameScreen:
attached gifts: Object {
"reciptId": "baNlCz6KFVABxYNHAHasd213Fu1",
"walletId": "KQCqSqC3cowZ987663QJboZ",
}
What could possibly be the reason behind this and how do I solve this?
EDIT
Added related code here: https://snack.expo.dev/uKfDPpNDr
From the docs
When you call useEffect in your component, this is effectively queuing
or scheduling an effect to maybe run, after the render is done.
After rendering finishes, useEffect will check the list of dependency
values against the values from the last render, and will call your
effect function if any one of them has changed.
You might want to take a different approach to this.
There is not much info, but I can try to suggest to put it into render, so it might look like this
const filterAttachedGifts = useMemo(() => ...your function from useEffect... , [attachedGitfs])
Some where in render you use "data" variable to render attached gifts, instead, put filterAttachedGifts function there.
Or run this function in component body and then render the result.
const filteredAttachedGifts = filterAttachedGifts()
It would run on first render and also would change on each attachedGifts change.
If this approach doesn't seems like something that you expected, please, provide more code and details
UPDATED
I assume that the problem is that your wallet receive attachedGifts on first render, and after it, useEffect check if that value was changed, and it doesn't, so it wouldn't run a function.
You can try to move your function from useEffect into external function and use that function in 2 places, in useEffect and in wallet state as a default value
feel free to pick up a better name instead of "getUpdatedArray"
const getUpdatedArray = () => {
const updatedArray = [...walletData];
if (attachedGifts !== null) {
let selectedIndex = -1
updatedArray = updatedArray.map((val: IWalletListDT) => {
if (val?.walletId === attachedGifts?.walletIds) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
setPurchaseDetailDialog(val);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
}
return updatedArray;
}
Then use it here
const [walletData, setWalletData] = useState(getUpdatedArray());
and in your useEffect
useEffect(() => {
setWalletData(getUpdatedArray());
}, [attachedGifts]);
That update should cover the data on first render. That might be not the best solution, but it might help you. Better solution require more code\time etc.

REDUX: store variables accessed via this.props are outdated but store.getState() works

Well hello there!
I'm having some issues - that I never had before - by accessing store variables through mapStateToProps. Namely, they never change and always have their default value I setup in the store BEFORE changing them in any way. If I call them by store.getState().reducer.x my code works!
Here's my store:
export const initialState = {
isKeyManagementWindowOpen: false
};
const rootReducer = combineReducers({
some: someReducer,
settings: settingsComponentReducer
)};
const store = createStore(rootReducer, compose(applyMiddleware(thunk), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : variable => variable));
export default store;
settingsComponentActions.js
export const TOGGLE_KEY_MANAGEMENT_WINDOW = 'TOGGLE_KEY_MANAGEMENT_WINDOW';
export const toggleKeyManagementWindow = isKeyManagementWindowOpen => {
return { type: TOGGLE_KEY_MANAGEMENT_WINDOW, isKeyManagementWindowOpen};
}
settingsComponentReducer.js
export const settingsComponentReducer = (state = initialState, action) => {
console.log(action);
switch (action.type) {
case Actions.TOGGLE_KEY_MANAGEMENT_WINDOW:
return Object.assign({}, state, {
isKeyManagementWindowOpen: action.isKeyManagementWindowOpen
});
default: return state;
}
};
One thing that may be causing issues is that I am calling this.props in my websocket's subscribe method.
Key.js
connectToWebsocket = ip => {
const stompClient = Stomp.client(`url/receivekey`);
stompClient.heartbeat.outgoing = 0;
stompClient.heartbeat.incoming = 0;
stompClient.debug = () => null;
stompClient.connect({ name: ip }, frame => this.stompSuccessCallBack(frame, stompClient), err => this.stompFailureCallBack(err, ip));
}
stompSuccessCallBack = (frame, stompClient) => {
stompClient.subscribe(KEY_READER_NODE, keyData => {
if (!this.props.isKeyManagementWindowOpen) {
this.loginWithKey(keyData.body);
} else {
this.addToKeyList(keyData.body);
}
});
}
Even though I set isKeyManagementWindowOpen beforehand to true it still resolves to false. If I swap !this.props.isKeyManagementWindowOpen with !store.getState().settings.isKeyManagementWindowOpen the code works and it goes into this.addToKeyList(keyData.body).
So, if I swap those but LEAVE every store call in this.addToKeyList as this.props. then those are all default valued too, which doesn't make sense. It only works if I swap every this.props. line with store.getState()....
const mapStateToProps = state => ({
...
...
isKeyManagementWindowOpen: state.settings.isKeyManagementWindowOpen,
});
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Key));
As of now, my code works but I'd like to call the props as this.props... and not via store.getState().... Any idea why this could happen?
Thanks!
Seems like you're using deep state
Object assign only makes shallow copies of objects. So let's try to eliminate the easiest possible cause.
export const settingsComponentReducer = (state = initialState, action) => {
const newState = JSON.parse(JSON.stringify(state));
Then use newState instead of state below.
This will make a deep copy of your state and will always be a new object forcing your app to see it as a new prop and re-render correctly.
Why not use something like this, as you shouldn't directly mutate the overall state of the app, only update it if an action is triggered but spread the original state in prior to updating.
export const settingsComponentReducer = (state = initialState, action) => {
console.log(action);
switch (action.type) {
case Actions.TOGGLE_KEY_MANAGEMENT_WINDOW:
return {
...state,
isKeyManagementWindowOpen: action.isKeyManagementWindowOpen
});
default:
return state;
}
};
The problem is that React cannot have updated any value in this.props by the time the next line of code has executed.
This is not actually a Redux-specific problem. In any React component, triggering a state change on a line will still result in the same props and state values on the next line, because the current function is still executing and React has not re-rendered yet.

Click function being called twice but only on the first click?

I'm passing an action down form Context. The onClick has to go a few levels but for some reason when you click it the first time it fires twice. Only the first time, though, after that it fires once per normal. It also only seems to do this if the console.log is inside the Reducer function...
Demo can be found here:
https://codesandbox.io/s/nameless-haze-veejt
You push reducer of useReducer to out of ImagesProvider
Example code file ImagesContext
import React, { createContext, useReducer } from "react";
const initialState = {
galleries: [
{
images: ["screenshot-1.jpg", "screenshot-2.jpg"]
},
{
images: [
"screenshot-3.jpg",
"screenshot-4.jpg",
"screenshot-5.jpg",
"screenshot-6.jpg"
]
}
]
};
const Images = createContext(initialState);
const { Provider } = Images;
const reducer = (state, action) => {
switch (true) {
case action.type === "removeImage":
console.log("click", action.id);
return { ...state };
default:
throw new Error(`Unhandled type: ${action.type}`);
}
}
const ImagesProvider = ({ children }) => {
const [state, updater] = useReducer(reducer, initialState);
return <Provider value={{ state, updater }}>{children}</Provider>;
};
export { Images, ImagesProvider };
codepen url: https://codesandbox.io/s/unruffled-dust-qmrxm?fontsize=14&hidenavigation=1&theme=dark
Good luck!

How do you destructure a React useState hook into a namespace?

As a personal preference I wrap React props in namespaces. It helps me organize where different props come from.
With the useState hook I'm doing this.
function MyComponent() {
const [todoCount, setTodoCount] = useState(100);
const [doneCount, setDoneCount] = useState(0);
const myState = {
todoCount,
setTodoCount,
doneCount,
setDoneCount
};
return (
<>
<Text>Todo {myState.todoCount}</Text>
<Text>Done {myState.doneCount}</Text>
</>
);
}
Is there a more succinct syntax for state setup?
My failed attempt was
const myState = {
[todoCount, setTodoCount]: useState(100),
[doneCount, setDoneCount]: useState(0);
};
Sounds like the type of thing you could do as part of a custom hook e.g.
function useMappedState(defaultState = {}) {
const keys = Object.keys(defaultState);
return keys.reduce((map, k) => {
const fk = `set${k.charAt(0).toUpperCase()}${k.slice(1)}`;
const [state, setState] = useState(defaultState[k]);
map[k] = state;
map[fk] = setState;
return map;
}, {});
}
...
const state = useMappedState({
todoCount: 100,
doneCount: 0
});
console.log(state.todoCount) // 100
state.setTodoCount(5); // should set state of todoCount
In theory, this should give you what you want, but I've not tested so use with caution (e.g. I'm not even sure if hooks can be called can be called inside an iterator). - this works fine.
Although, what you are doing is really similar to what useReducer already does, might be worth some experimenting with that hook instead.
When you need to manage complex state, useReducer is the goto. It is a hook which accepts a reducer function in addition to initial state. The reducer is written by you to map certain "actions" to changes in state. You can "dispatch" an action to the reducer function to update state according to the rules you specify. useState itself internally calls useReducer.
/* action = { type: string, payload: any type } */
function reducer(state, { type, payload }) {
switch(type) {
case 'do-todo':
return { doneCount: state.doneCount + 1, todoCount: state.todoCount - 1 }
case 'undo-todo':
return { doneCount: state.doneCount - 1, todoCount: state.todoCount + 1 }
default:
return state
}
}
function App() {
const [ state, dispatch ] = useReducer(reducer, { todoCount: 100, doneCount: 0 })
return (
<>
<Text>Todo {state.todoCount}</Text>
<Text>Done {state.doneCount}</Text>
<Button onClick={() => dispatch({ type: 'do-todo' })}>Do Todo</Button>
</>
);
}
You can use multiple reducers to map to multiple namespaces.

React doesn't recognize state change in reducer

I have a component that makes an API call and then updates the state through a reducer. The problem is, this doesn't work so well cause the data don't get updated in the component, it's like the react didn't notice a state change a never re-rendered the component, but I'm not sure if that's the real issue here. So the component looks like this:
class MyComponent extends Component {
componentDidMount() {
// ajax call
this.props.loadData(1);
}
render() {
return (
<Grid>
<MySecondComponent
currentData={this.props.currentData}
/>
</Grid>
);
}
}
const mapStateToProps = state => ({
reducer state.myReducer,
currentData: state.myReducer.currentData
});
const mapDispatchToProps = dispatch => {
return {
loadData: () => {
HttpClient.getData(id, (data) => {
dispatch(
action_loadCurrentData(
data
)
);
});
},
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(MyComponent);
I am doing 2 things here: issuing an API call as soon as component is mounted, and then after data is fetched, dispatching action_loadCurrentData
This action looks like this:
//Action
export function action_loadCurrentData(
data
) {
return {
type: 'LOAD_CURRENT_DATA',
payload: {
currentData: data,
}
};
}
and the reducer:
//Reducer
const defaultState = {
};
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'LOAD_CURRENT_DATA':
state = {
...state,
currentData: {
myData: {
...state.currentData.myData,
0: action.payload.currentData
}
}
};
}
};
export default myReducer;
So the issue here is that the this.props.currentData that I'm passing to MySecondComponent will end up empty, as if I didn't set the data at all. However, If I stop the execution in the debugger and give it a few seconds, the data will be populated correctly, so I'm not sure what I'm doing wrong here?
Don't reassign state, return the newly created object instead
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'LOAD_CURRENT_DATA':
return {
...state,
currentData: {
myData: {
...state.currentData.myData,
0: action.payload.currentData
}
}
};
}
};
Your reducer needs to return the new state object, which needs to be a different instance from the previous state to trigger components update.
According to redux documentation:
The reducer is a pure function that takes the previous state and an action, and returns the next state.
And
Things you should never do inside a reducer:
Mutate its arguments;
Perform side effects like API calls and routing transitions;
Call non-pure functions, e.g. Date.now() or Math.random().

Categories

Resources