variable updated by redux state does not trigger useEffect
not sure what i am missing but i can see state.user.fen updating but it does not trigger useEffect to be called?
export default function BoardSquare({ piece, black, position,isFromSquare,isToSquare}) {
dispatch(setFen(fen))
}
//userActions.js
export const setFen = (fen) => (dispatch) => {
dispatch({
type: SET_FEN,
payload: fen,
});
}
//userReducer.js
export default function userReducer(state = initialState,action) {
switch(action.type){
case SET_FEN:
return{
...state,
fen: action.payload
}
}
function GameApp() {
const fen = useSelector(state => state.user.fen)
useEffect(() => {
alert("should be working now ?")
console.log("should be working now ??????")
setBoard(fen)
}, [fen])
}
Related
I have infinity loop when using custom hook and reducer in React Hook. Custom hook has been infinity loop when moving dispatch setLoading into async function. But when I calling dispatch action out of async function scope, I have related issue with dispatch action. setLoading(false) will called before async function has been completed. Please help me clarify it
That is my code
reducer:
import { SET_ERROR, SET_LOADING } from "./common.type";
export const commonInitial = {
isLoading: false,
isError: {
message: "",
status: "",
},
};
const commonReducer = (state: any, action: any) => {
switch (action.type) {
case SET_LOADING:
return { ...state, isLoading: action.payload };
case SET_ERROR:
return { ...state, isError: action.payload };
default:
return { ...state };
}
};
export default commonReducer;
useFetch (custom hook)
export const useFetch = (url: string) => {
const [response, setResponse] = useState([]);
const [error, setErr] = useState({ message: "", status: "" });
//fetching state handler
const { dispatch } = useContext(CommonContext);
useEffect(() => {
const fetchDataFromUrl = async () => {
//show loading
dispatch(setLoading(true));
try {
const { data } = await axios.get(url);
setResponse(data);
dispatch(setLoading(false));
} catch (err: any) {
//handle error
dispatch(setLoading(false));
setErr(err);
}
};
fetchDataFromUrl();
}, [url]);
useEffect(() => {
dispatch(setError({ message: error.message }));
}, [error]);
return [response];
};
hook called
// using custom hook fetch data
const [data] = useFetch("https://jsonplaceholder.typicode.com/posts");
return (
<div>
<ul>
{!!data?.length &&
data.map((item: Post) => {
return (
<div key={item.id}>
<Link to={`/post/${item.id}`}>Moves to Post {item.id}</Link>
</div>
);
})}
</ul>
</div>
);
};
export default DemoHookA;
I am not sure, but the issue could be because of useEffect hook. As per React 18 in invoked multiple types. For getting rid of this rendering issue, you can create a ref that would check if the component is mounted or not and updating the ref once it is mounted.
You could achive this by following way.
import React, {
useEffect,
useRef
} from 'react';
function YourComponent() {
const isRendered = useRef(false);
// UseEffect
useEffect(() => {
if (!isRendered.current) {
// Your code goes here...
}
return () => {
// Update isRendered
isRendered.current = true;
}
})
}
export default YourComponent;
Put all the code that you have inside useEffect inside the if condition.
Hope it will work for you.
Can anyone help me to update state with timeout in react reducer.
I don't have much experience even with pure javascript, so I can hardly find an answer myself at this moment.
In my first ever react app (with useContex and useReducer) i have simple BUTTON checkbox with onClick function to dispatch type in reducer:
<ToggleButton
className="mb-2"
id="Getdocs"
type="checkbox"
variant="outline-secondary"
size="sm"
checked={Getdocs}
onChange={(e) => Getdocsaction()}
>
Render documents
</ToggleButton>
In my context.js i have:
import React, { useContext, useReducer} from 'react'
import reducer from './reducer'
const AppContext = React.createContext()
const initialState = {
.
.
.
Showdocs: false,
.
.
.
}
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
...
const Getdocsaction = () => {
dispatch({ type: 'GET_DOCS' })
}
...
return (
<AppContext.Provider
value={{
...state,
Getdocsaction
}}
>
{children}
</AppContext.Provider>
)
}
export const useGlobalContext = () => {
return useContext(AppContext)
}
export { AppContext, AppProvider }
In reducer.js i have:
const reducer = (state, action) => {
if (action.type === 'GET_DOCS') {
let newPassports = state.oldDocs.filter((doc) => doc.passport === true);
if (newPassports.length === 0) {
state.Passports = []
state.Showdocs = true
state.Getdocs = false /uncheck checkbox button
setTimeout(() => {
state.Showdocs = false //wont update this
console.log("setTimeout fired") //logs in console after 5 secs
}, 5000)
return { ...state }
}
if (newPassports.length !== 0) {
return { ...state, Passports: newPassports, Showdocs: true, Getdocs: !state.Getdocs }
}
return { ...state }
}
throw new Error('no matching action type')
}
export default reducer
Finally, in my App.js i check if Showdocs is true or false and return the rest (return the passports from updated array or bootstrap alert if there is no values in array (Passport.length === 0) )
What i am trying to achieve is that when i have empty Passports array i want set Showdocs: true (in order to show alert msg) and set it back to false after 5 secs (in order to remove msg ...)
Any help is welcome and even keywords by which i could research this issue.
Thank you.
Reducers are intended to be “pure” and synchronous, and they shouldn't mutate input arguments. Since mutating state after a delay is a side-effect, you should consider instead handling this in a useEffect hook separately.
E.g.:
const SomeComponent = () => {
const [state, dispatch] = useReducer(reducer)
const { hideDocsAfterDelay } = state
useEffect(() => {
if (!hideDocsAfterDelay) return
const timer = setTimeout(() => {
dispatch({ TYPE: "HIDE_DOCS" })
}, 5000)
return () => { clearTimeout(timer) }
}, [hideDocsAfterDelay])
// …
}
In this scenario, you would set a hideDocsAfterDelay property in your state to trigger the timer and another action handler that would set showDocs and hideDocsAfterDelay to false.
I think you should implement an action that basically updates the state with this state.Showdocs = false and then dispatch this action inside a setTimeout.
So basically change Getdocsaction to this:
const Getdocsaction = () => {
dispatch({ type: 'GET_DOCS' })
setTimeout(() => {dispatch({type: 'The action that sets Showdocs to false'})}, 5000);
}
I have this simple React custom hook with a data list and remove/undo funcionality.
Now, I need to call a delete request on cleanup. The problem is the cleanup is called as an async onClose callback with a timeout delay, so when I delete an item and go to another page before the timeout runs out, the dispatch/reducer is not being called. I need to call the API delete request with the updated state.toRemove array which is only accessible in the reducer, even after the component unmounts.
The logs in the example code shows that after unmount, the cleanup function is being called, however the reducer with the updated state is not.
This example is based on React Toastify undo funcionality (which has no delete API calls)
Is this just a React limitation or is there some solution to it? Thank you!
import React, { useState, useEffect, useReducer } from 'react'
import { toast } from 'react-toastify'
function reducer(state, action) {
console.log("I'm not being called after component unmount.")
switch (action.type) {
case "LOAD_COLLECTION":
return {
collection: action.collection,
toRemove: state.toRemove
};
case "QUEUE_FOR_REMOVAL":
return {
collection: state.collection,
toRemove: [...state.toRemove, action.id]
};
case "CLEAN_COLLECTION": {
console.log("I'm not being called neither. API delete would go here.")
return {
collection: state.collection.filter(
v => !state.toRemove.includes(v.id)
),
toRemove: []
};
}
case "UNDO":
return {
collection: state.collection,
toRemove: state.toRemove.filter(v => v !== action.id)
};
default:
return state;
}
}
export default function useList() {
const [state, dispatch] = useReducer(reducer, { collection: [], toRemove: [] });
useEffect(() => {
// API CALL...
dispatch({ collecion, type: "LOAD_COLLECTION" })
}, [])
const undo = (id) => {
dispatch({ id, type: "UNDO" })
}
const remove = (id) => {
dispatch({ id, type: "QUEUE_FOR_REMOVAL" });
toast(<Undo id={id} text="Removed" undo={undo} />, {
onClose: clean,
})
}
const clean = () => {
console.log("I'm being called after the toast timeout, even after unmount.")
dispatch({ type: "CLEAN_COLLECTION" })
}
return {
remove,
list: state.collection.filter((s) => !state.toRemove.includes(s.id))
}
}
My axios transaction is all done in the redux actions so that I can re-use the function. The issue is that, I need to fetch the data first which is done by redux and then re-assign the value in a state, but the data cannot be populated in the state. Below is how my code looks like.
Setting.js
...
import { getUserDetail } from './redux/actions/settingActions';
export default function Setting() {
const dispatch = useDispatch()
const { user } = useSelector(state => state.settingReducer)
const [userDetail, setUserDetail] = useState()
useEffect(() => {
dispatch(getUserDetail())
setUserDetail(user) // I want to set the user here
}, [])
...
}
settingActions.js
export const getUserDetail = () => (dispatch, getState) => {
axios.get('url-goes-here')
.then(res => {
dispatch({
type: SET_USER_DETAIL,
payload: { res.data }
})
})
.catch(error => {
throw error;
})
}
settingReducer
function initialState() {
return {
...
user: {}
}
}
export default function (state = initialState(), action) {
const { type, payload } = action;
switch (type) {
case SET_USER_DETAIL:
return {
...state,
user: payload
}
default:
return state
}
}
My purpose of doing this is because I want to do some user details update but I want it to be done within the same file.
put user and dispatch as dependency in useEffect
useEffect(() => {
dispatch(getUserDetail())
setUserDetail(user)
}, [user,dispatch])
I'll spare you the broader context as it's pretty simple. Using React hooks, the first dispatch here fires automatically and works just fine, but the second one doesn't. Context is imported from another file, so I assume it's a (lowercase) context issue, but I don't know how to fix it?
const Component = props => {
const [, dispatch] = useContext(Context);
useEffect(() => {
document.title = state.title;
// eslint-disable-next-line react-hooks/exhaustive-deps
});
// this works
dispatch({ type: "UPDATE_TITLE", payload: "Not Click!" });
function onAddClick() {
// this doesn't work
dispatch({ type: "UPDATE_TITLE", payload: "CLICKED!" });
}
return (
<div>
<AddButton onClick={onAddClick} />
</div>
);
};
Here's the parent.
const Reducer = (state, action) => {
switch (action.type) {
case "UPDATE_TITLE":
state["title"] = action.payload;
return state;
default:
return state;
}
};
const initialState = {
title: "My Title"
};
export const Context = createContext(initialState);
const App = () => {
const [state, dispatch] = useReducer(Reducer, initialState);
return (
<Context.Provider value={[state, dispatch]}>
<Component />
</Context.Provider>
);
};
export default App;
Console logs fire in the correct reducer case in both cases, but only the one marked 'this works' will actually update the state properly, the other one fails silently.
Fixed: https://codesandbox.io/s/cranky-wescoff-9epf9?file=/src/App.js
I don't know what you trying to achieve by placing the dispatch outside controlled env (like an event or useEffect):-
// this works
dispatch({ type: "UPDATE_TITLE", payload: "Not Click!" });
// but it will run in infinite loop tho (no changes can be made then)
So the fixes should be:-
in Reducer, make sure not to completely mutate your state:-
const Reducer = (state, action) => {
switch (action.type) {
case "UPDATE_TITLE":
// Don't do this
// let newState = state;
// newState["page"]["title"] = action.payload;
// console.log("updating", newState, newState.page.title);
// return newState;
// Do this instead
return {
...state,
page: {
...state.page,
title: action.payload
}
};
default:
return state;
}
};
Place your dispatch for not Click! inside an event or function or better yet in this case, useEffect since you wanna apply it once the component rendered.
const Demo = props => {
const [state, dispatch] = useContext(Context);
useEffect(() => {
document.title = state.title;
// eslint-disable-next-line react-hooks/exhaustive-deps
// this works (should be here)
dispatch({ type: "UPDATE_TITLE", payload: "Not Click!" });
}, []); // run it once when render
// this works but, (should not be here tho - cause it will run non-stop)
// dispatch({ type: "UPDATE_TITLE", payload: "Not Click!" });
function onAddClick() {
// this will work now
dispatch({ type: "UPDATE_TITLE", payload: "CLICKED!" });
}
return (
<div>
<button onClick={onAddClick}>Click Me!</button>
<p>State now: {state.title}</p>
</div>
);
};
You can try and refer to this sandbox and see how it works.
EDITED & UPDATED sandbox
It looks like you are attempting to mutate state directly. Instead try to return a new object that is the result of the changes from the action applied to the old state.
const Reducer = (state, action) => {
switch (action.type) {
case "UPDATE_TITLE":
return {
...state,
page: {
...state.page,
title: action.payload
}
};
default:
return state;
}
};
Alternatively, use produce from immerjs to give you the ability to write your reducer in this mutable style.
import produce from "immer";
const Reducer = produce((state, action) => {
switch (action.type) {
case "UPDATE_TITLE":
state["title"] = action.payload;
break;
default:
return;
}
});