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);
}
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.
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])
}
I'm new to react-admin and I am trying to build a custom image gallery input. it should show a modal with images (data is already fetched and stored in the redux) so the user can select one or more images (upon selection an action is dispatched to update the reducer's value) and I need these selected images ids in the transform function on <Create /> so I can add the required data before dataProvider method is called.
but I have a weird issue, that might be because of my lack of react knowledge. in the snippet below, I try to get the useReducers value and then add it to the form.
import React, { useReducer, useMemo, useEffect, useCallback } from 'react';
import { Create as Ra_create } from 'react-admin';
const ctxInitialValues = {};
const galleryCtx = React.createContext(ctxInitialValues);
const CreateWithGallery = (props) => {
const [selectedImages, dispatch] = useReducer((state, { type, payload }) => {
switch (type) {
case 'UPDATE_STATE':
return { ...payload };
case 'INIT_RECORD':
return {
...state,
[payload]: [],
};
default:
return state;
}
}, ctxInitialValues);
const updateSelection = (record, image, operation) => {
if (operation === 'add') {
let newState = {
...selectedImages,
[record]: [...selectedImages[record], image],
};
dispatch({
type: 'UPDATE_STATE',
payload: newState,
});
} else if (operation === 'remove') {
let newState = {
...selectedImages,
[record]: selectedImages[record].filter((item) => item.id !== image.id),
};
dispatch({
type: 'UPDATE_STATE',
payload: newState,
});
}
};
const transformPayload = (data) => {
let transformed = {
...data,
};
// but I get {} here
for (let record in selectedImages) {
transformed[record] = selectedImages[record].map((item) => ({
id: item.id,
}));
}
return transformed;
};
useEffect(() => {
console.log(selectedImages);
// I get fresh values here
}, [selectedImages]);
const initializeRecord = (record) => {
dispatch({
type: 'INIT_RECORD',
payload: record,
});
};
return (
<galleryCtx.Provider
value={{
selectedImages,
updateSelection,
initializeRecord,
}}
>
<Ra_create {...props} transform={transformPayload}>
{props.children}
</Ra_create>
</galleryCtx.Provider>
);
};
export { galleryCtx };
export default CreateWithGallery;
when I try to access the selectedImages values in the transform function I get {}, which is the initial state. I have tried using useCallback and useMemo to make sure the values are changed after each dispatch but it did not make any difference.
there's also a similar behavior in this question as well:
React Admin: how to pass state to transform
how can I use state in the transform function?
I ended up with setting the transform prop on the component (in custom toolbar):
const CustomToolbar = (props: any) => {
const transform = useCallback((data: any) => {
return {
...data,
files: something_from_state,
};
}, [something_from_state]);
const handleClick = () => {
};
return <Toolbar {...props}>
<SaveButton
handleSubmitWithRedirect={handleClick} transform={transform}/>
</Toolbar>
};
to fix this you can use transform prop on as explained in the react-admin docs. it is still unclear though, why we can't get state in the transform function on the or .
I'm having issue where hook is being used in multiple files and it is being called twice for useEffect before the 1st one's async method finish (which should block the 2nd hook call, but it's not). See below 2 scenarios:
Stack Navigator
const { context, state } = useLobby(); // Hook is called here 1st, which will do the initial render and checks
return (
<LobbyContext.Provider value={context}>
<LobbyStack.Navigator>
{state.roomId
? <LobbyStack.Screen name="Lobby" component={LobbyScreen} />
: <LobbyStack.Screen name="Queue" component={QueueScreen} />
}
</LobbyStack.Navigator>
</LobbyContext.Provider>
)
Lobby Hooks
export const useLobby = () => {
const [state, dispatch] = React.useReducer(...)
//
// Scenario 1
// This get called twice (adds user to room twice)
//
React.useEffect(() => {
if (!state.isActive) assignRoom();
}, [state.isActive])
const assignRoom = async () => {
// dispatch room id
}
const context = React.useMemo(() => ({
join: () => { assignRoom(); }
})
}
Queue Screen
const { context, state } = useLobby(); // Hook is called here 2nd right after checking state from stack navigator
//
// Scenario 2
// Only does it once, however after state is changed to active
// the stack navigator didn't get re-render like it did in Scenario 1
//
React.useEffect(() => {
roomLobby.join();
}, []);
return (
...
{state.isActive
? "Show the room Id"
: "Check again"
...
)
In scenario 1, I guess while 1st hook is called and useEffect is doing async to add user to the room and set active to true. Meanwhile the conditional render part is moving straight to Queue screen which calls the hook again and doing the useEffect (since 1st haven't finished and isActive is still false).
How can I properly setup useReducer and useMemo so that it renders the screen base on the state.
Edited codes based on the answer
/* LobbyProvider */
const LobbyContext = React.createContext();
const lobbyReducer = (state, action) => {
switch (action.type) {
case 'SET_LOBBY':
return {
...state,
isActive: action.active,
lobby: action.lobby
};
case 'SET_ROOM':
return {
...state,
isQueued: action.queue,
roomId: action.roomId,
};
default:
return state;
}
}
const LobbyProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(lobbyReducer, initialState);
React.useEffect(() => {
console.log("Provider:", state)
if (!state.isActive) joinRoom();
}, [])
// Using Firebase functions
const joinRoom = async () => {
try {
const response = await functions().httpsCallable('getActiveLobby')();
if (response) {
dispatch({ type: 'SET_LOBBY', active: true, lobby: response.data })
const room = await functions().httpsCallable('assignRoom')({ id: response.data.id });
dispatch({ type: 'SET_ROOM', queue: false, roomId: room.data.id })
}
} catch (e) {
console.error(e);
}
}
return (
<LobbyContext.Provider value={{state, dispatch}}>
{ children }
</LobbyContext.Provider>
)
}
/* StackNavigator */
const {state} = React.useContext(LobbyContext);
return (
<LobbyProvider>
// same as above <LobbyStack.Navigator>
// state doesn't seem to be updated here or to re-render
</LobbyProvider>
);
/* Queue Screen */
const {state} = React.useContext(LobbyContext);
// accessing state.isActive to do some conditional rendering
// which roomId does get rendered after dispatch
You must note that a custom hook will create a new instance of state everytime its called.
For example, you call the hook in StackNavigator component and then again in QueueScreen, so 2 different useReducers will be invoked instead of them sharing the states.
You should instead use useReducer in StackNavigator's parent and then utilize that as context within useLobby hook
const LobbyStateContext = React.createContext();
const Component = ({children}) => {
const [state, dispatch] = React.useReducer(...)
return (
<LobbyStateContext.Provider value={[state, dispatch]]>
{children}
</LobbyStateContext>
)
}
and use it like
<Component>
<StackNavigator />
</Component>
useLobby will then look like
export const useLobby = () => {
const [state, dispatch] = React.useContext(LobbyStateContext)
const assignRoom = async () => {
// dispatch room id
}
const context = React.useMemo(() => ({
join: () => { assignRoom(); }
})
return { context, assignRoom, state};
}
StackNavigator will utilize useLobby and have the useEFfect logic
const { context, state, assignRoom } = useLobby();
React.useEffect(() => {
if (!state.isActive) assignRoom();
}, [state.isActive])
return (
<LobbyContext.Provider value={context}>
<LobbyStack.Navigator>
{state.roomId
? <LobbyStack.Screen name="Lobby" component={LobbyScreen} />
: <LobbyStack.Screen name="Queue" component={QueueScreen} />
}
</LobbyStack.Navigator>
</LobbyContext.Provider>
)
A child component has the following button code:
// SelectDonation.js
<button
onClick={(e) => {
e.preventDefault();
this.props.testThunk();
console.log(store.getState());
}}
>Test thunks</button>
this.props.testThunk() does not update the state object. I connected Redux Thunk like so:
// reducer.js
import ReduxThunk from "redux-thunk";
const starting_state = {
log_to_console : 0,
donation_amount : 12,
checkoutStep : 'selectDonation',
};
const reducer = (previous_state = starting_state, action) => {
switch (action.type) {
case 'thunkTest':
return {
...previous_state,
redux_thunk_test_var : action.payload
};
default:
return previous_state;
}
};
export default createStore(reducer, starting_state, applyMiddleware(ReduxThunk));
I expect a new state property redux_thunk_test_var to display in state but it does not onClick. I do see the state variables with initial states in the console though.
Am I not passing down the thunk correctly? Here is App.js
// App.js
{this.props.checkoutStep === checkoutSteps.selectDonation &&
<SelectDonation
dispatch_set_donation_amount = {this.props.dispatch_set_donation_amount}
dispatchChangeCheckoutStep={this.props.dispatchChangeCheckoutStep}
{...this.props}
/>
}
</Modal>
</header>
</div>
);
}
}
const map_state_to_props = (state) => {
return {
log_prop : state.log_to_console,
donation_amount : state.donation_amount,
checkoutStep : state.checkoutStep,
}
};
const map_dispatch_to_props = (dispatch, own_props) => {
return {
dispatch_set_donation_amount : amount => dispatch(set_donation_amount(amount)),
dispatchChangeCheckoutStep : newStep => dispatch(changeCheckoutStep(newStep)),
dispatchUpdateStateData : (stateData, stateVariable) => (dispatch(updateStateData(stateData, stateVariable))),
testThunk
}
};
The action thunk:
// actions.js
export const testThunk = () => {
const testDelay = setTimeout(() => 'Set Timeout done', 2000);
return (dispatch) => {
testDelay.then((data) => dispatch({
type: 'thunkTest',
payload: data })
)
}
};
You need to dispatch the result of the testThunk() action creator. Right now, you're just returning it, and not calling dispatch(testThunk()).
See this gist comparing syntaxes for dispatching to help understand the issue better.
The best way to fix this is to use the "object shorthand" form of mapDispatch. As part of that, I suggest changing the prop names to remove the word "dispatch", which lets you use the simpler ES6 object literal syntax:
const map_dispatch_to_props = {
set_donation_amount,
changeCheckoutStep,
updateStateData,
testThunk,
};
conponentDidMount() {
this.props.testThunk();
}
const map_dispatch_props = {
testThunk
}
//action creator
const fetch = (data) => ({
type: 'thunkTest',
payload: data
})
const fakeFetch = () => new Promise((resolve, reject) => setTimeout(() => resolve('Set Timeout done'), 2000));
export const testThunk = () => (dispatch) => fakeFetch.then(data => dispatch(fetch(data)))