I am having a problem with redux state. The state won't reset after route change.
I have a component with "ForgottenPassword ", when I type in email and click on "reminde password" there is alert with text "Email have been send" (if success) or "Error occured" (if email is incorrect). When I go to login component and then comeback to "ForgottenPassword" component the alert with text (success or error) is still there cause the state did not reset.
Is there a way to listen to route change and set state to initial so the success or error message would dissapear after route change?
const ForgottenPassword = (props: Props) => {
const { loginUserLoading = false, forgottenPasswordAsync } = props;
const t = useTranslationById();
const formik = useFormik({
initialValues: {
email: "",
},
onSubmit: (values: ForgottenPasswordParams) => {
forgottenPasswordAsync(values);
},
});
React.useEffect(() => {
if (props.forgotPasswordLoadingSuccess) {
formik.resetForm();
}
}, [props.forgotPasswordLoadingSuccess]);
const handleSubmitButton = React.useCallback(() => formik.handleSubmit(), [formik]);
return (
<div className={styles["forgotten-password-form"]}>
<div className={styles["forgotten-password-form__title"]}>{t("user-forgotten-password__title")}</div>
<form onSubmit={formik.handleSubmit}>
<Input
label={t("user-login__email-label")}
placeholder={t("user-login__email-label")}
name="email"
type="text"
/>
{props.forgotPasswordLoadingError && <div className={styles["forgotten-password-form__error"]}><FormattedMessage id="user-recover-password-error" /></div>}
{props.forgotPasswordLoadingSuccess && <div className={styles["forgotten-password-form__success"]}><FormattedMessage id="user-recover-password-email-send" /></div>}
<Button>
Remind password
</Button>
</form>
</div>
);
};
export default ForgottenPassword;
export const forgottenPassword = createAsyncAction(
"FORGOTTEN_PASSWORD_REQUEST",
"FORGOTTEN_PASSWORD_SUCCESS",
"FORGOTTEN_PASSWORD_FAILURE"
)<void, any, ApiError>();
const reducer = (state: UserState = {}, action: UserAction) => {
return produce(state, (draft) => {
switch (action.type) {
case getType(forgottenPassword.request):
draft.forgottenPasswordLoading = true;
break;
case getType(forgottenPassword.success):
draft.forgottenPasswordLoading = false;
draft.forgottenPasswordLoadingSuccess = true;
break;
case getType(forgottenPassword.failure):
draft.forgottenPasswordLoading = false;
draft.forgottenPasswordLoadingError = action.payload;
break;
}
});
};
const mapStateToProps = (state: RootState) => ({
forgotPasswordLoading: selectForgotPasswordLoading(state),
forgotPasswordLoadingSuccess: selectForgotPasswordLoadingSuccess(state),
forgotPasswordLoadingError: selectForgotPasswordLoadingError(state),
loginUserLoading: selectLoginUserLoading(state),
loginUserLoadingError: selectLoginUserLoadingError(state),
});
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
forgottenPasswordAsync,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(ForgottenPassword);
export const selectForgotPasswordLoading = createSelector(selectState, (state) => state.forgottenPasswordLoading);
export const selectForgotPasswordLoadingSuccess = createSelector(selectState, (state) => state.forgottenPasswordLoadingSuccess);
export const selectForgotPasswordLoadingError = createSelector(selectState, (state) => state.forgottenPasswordLoadingError);
I am new here so sorry if I ask incorrectly.
The code is very complex and it is hard to paste only a sample of the code.
I was trying to do if else statemnt in Forgotten component but I figure out it won't work cause it is a problem that lies in redux, which I am starting to learn.
If you want to reset the state when the route is changed you can use the useEffect hook in the role of a componentWillUnmount in the page that is getting destroyed (in this case I suppose it is ForgottenPassword).
You can manage the reset as you prefer. A solution can be adding an action to your redux state like FORGOTTEN_PASSWORD_RESET which resets the state, and then dispatching the action from the hook.
You can write:
//ForgottenPassword.jsx
const ForgottenPassword = () => {
//existing code...
React.useEffect(()=>{
return () => {
//This will be called only when the page is destroyed.
reset() //reset your redux state here...
}
},[])
return (
//component's code...
);
}
const mapDispatchToProps = (dispatch) => {
return ({
reset: () => dispatch({ type: "FORGOTTEN_PASSWORD_RESET"}),
//OTHER ACTIONS....
})
}
export default connect(mapStateToProps, mapDispatchToProps)(ForgottenPassword);
TWO NOTES on the code above:
Update your reducer to manage the new action, otherwise is useless. I've not fully understood how it works otherwise I would have updated myself
Usually, to keep the code "clean", the action should be stored in a separate file from the actual component and then imported.
Related
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 a React modal component that shows/hides depending on a variable that can be set/changed via a Redux dispatch.
I am at a bit of a loss of how to actually test it though.
Any help is appreciated.
Action
const HIDE_MODAL = 'HIDE_MODAL';
export const hideModal = () => ({
type: HIDE_MODAL
});
Reducer
// import HIDE_MODAL
export default (state = {}, action) => {
let newState = deepClone(state);
switch (action.type) {
case HIDE_MODAL:
newState.showModal = false;
break;
default:
return state;
}
return newState;
}
Component
const mapStateToProps = (state) => ({
showModal: state.showModal
});
const mapDispatchToProps = dispatch => ({
closeModal: () => dispatch(hideModal)
});
const MyComponent = ({closeModal, showModal}) => {
if (showModal) {
return <div>
<div>My Modal</div>
<button onClick={closeModal}/>Close</button>
</div>
}
);
Test
it('Should hide modal on click', () => {
const store = {showModal: true};
render(
<Provider store={mockStore(store)}>
<MyComponent />
</Provider>
);
expect(screen.queryByText('My modal')).toBeTruthy();
userEvent.click(screen.queryByText('Close'));
expect(screen.queryByText('My modal')).toBeFalsy();
});
first you should set your default value of showModal to true for testing purposes. If I'm not mistaken your function closeModal should be a little bit changes to this:
const mapDispatchToProps = dispatch => ({
closeModal: () => dispatch(hideModal()),
});
Because hideModal is a function you should call it too.
export const hideModal = () => ({
type: HIDE_MODAL
});
Otherwise you should pass an action directly to your dispatch Redux function(because dispatch takes an object with at least type key as an argument for proper work):
const mapDispatchToProps = dispatch => ({
closeModal: () => dispatch({
type: HIDE_MODAL
}),
});
Hope it will help.
I am trying to write a library that processes dispatched useReducer actions. However, they need to be processed only once I have the new react state.
The idea in the following example is to buffer all the actions as they happen and then after the component rerenders get the new state and send all the buffered actions with the new state into the processor (simple logger function in this case). Problem is, that won't work if the component bails out of rendering, because the component won't rerender and so the useEffect callback won't get called either.
function logger(action, state) {
// this should log every action immediately once its processed by react
// state must be up-to-date (after all batched state changes are processed)
console.log({action, state});
}
const buffer = [];
const Component = () => {
const [state, dispatch] = useReducer(
(state, action) => {
// bail out if there is no change
if (action.value === state.someState) return state;
return {someState: action.value};
},
{someState: 1}
);
const loggedDispatch = (action) => {
dispatch(action);
// Can't run the logger here as I don't have the new state yet
// so I am buffering the actions instead for later
buffer.push(action);
};
useEffect(() => {
// Now I have the new state, so I can run the logger
// Problem is that this won't run if the reducer bailed out of render
while (buffer.length) {
logger(buffer.shift(), state);
}
});
return (
<div>
<div onClick={() => loggedDispatch({value: 1})}>Set to 1</div>
<div onClick={() => loggedDispatch({value: 2})}>Set to 2</div>
</div>
);
};
Any idea how to fix this with react hooks?
With class components it was possible to use
this.setState(state => state, function () {
console.log('This runs regardless of bailout');
})
But react hooks such as useState or useReducer don't support the second parameter.
Codesandbox: https://codesandbox.io/s/friendly-pine-lebrb?file=/src/App.js
I think it is related to initial value that in {someState: 1} equal to value of first div button because when I click the second div button everything is going to be good.
this when I click first button
and this when click the second
Bailing out of a dispatch
If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
this is the quote of react useReducer about return same value witch maybe your problem.
I hope this useful.
You can create a custom hook that will use useReducer internally. In addition to the default functionality of useReducer it'll pass the previous state, next state and the action dispatched to your middleware.
Codesandbox: https://codesandbox.io/s/use-reducer-with-middleware-y2hvb?file=/src/index.js
const {
useCallback,
useReducer
} = React;
const {
render
} = ReactDOM
const useReducerWithMiddleware = (reducer, initialState, ...middlewares) => {
const [state, dispatch] = useReducer((prevState, action) => {
const nextState = reducer(prevState, action);
middlewares.forEach((middleware) =>
middleware(prevState, nextState, action)
);
return nextState;
}, initialState);
return [state, dispatch];
};
const reducer = (state, action) => {
if (action.type === CHANGE_TEXT_ACTION_TYPE) {
return { ...state,
text: action.payload
};
}
return state;
};
const initialState = {
text: "foo"
};
const loggerMiddleware = (prevState, nextState, action) => {
console.log(`${action.type} is fired with payload = ${action.payload}.`);
console.log(`prev state is equal to \n${JSON.stringify(prevState, null, 2)}`);
console.log(`next state is equal to \n${JSON.stringify(nextState, null, 2)}`);
};
const CHANGE_TEXT_ACTION_TYPE = "changeText";
const App = () => {
const [state, dispatch] = useReducerWithMiddleware(
reducer,
initialState,
loggerMiddleware
);
const handleButtonClick = useCallback(() => {
dispatch({
type: CHANGE_TEXT_ACTION_TYPE,
payload: state.text === "foo" ? "bar" : "foo"
});
}, [dispatch, state]);
return <button onClick={handleButtonClick}>{state.text}</button>;
};
render(<App /> , document.getElementById("app"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<body>
<div id="app"></div>
</body>
You can create a custom hook and use that,
I have created a sample, check this out this might help
https://stackblitz.com/edit/react-dispatcher
I have given suggestion in GitHub issue as well
https://github.com/facebook/react/issues/15344#issuecomment-868204394
import { useReducer, useEffect } from "react";
let cb = () => {};
function useDispatch() {
const [state, dispatch] = useReducer(
(state, action) => {
if (action.value === state.someState) return state;
return { someState: action.value };
},
{ someState: 0 }
);
useEffect(() => {
cb(state);
}, [state]);
return [
state,
(action, callback) => {
cb = callback;
dispatch(action);
},
];
}
const Component = () => {
const [state, dispatch] = useDispatch();
const loggedDispatch = (action) => {
console.log("pre-state", state);
dispatch(action, (newState) => {
console.log("new-state", newState);
});
};
return (
<div>
<div onClick={() => loggedDispatch({ value: 1 })}>Set to 1</div>
<div onClick={() => loggedDispatch({ value: 2 })}>Set to 2</div>
</div>
);
};
export default function App() {
return (
<div className="App">
<Component />
</div>
);
}
I use React-Redux and functional components with hooks.
Whenever I send a request for fetching data from the server I set isLoading field in global state to true and then after request is done back to false.
In the rendering component I either show the fetched data or a loading screen if isLoading is set to true. Now the problem is whenever I change page and then reopen the sreen with data, before showing loading screen react renders previous state data from the previous fetch for a brief moment.
Hope someone can explain to me how to avoid that behavior and the reasons why it acts that way.
Here's an example code of what I'm talking about. If I change routes and go back I can see the old data:
//App.js
function App() {
return (
<BrowserRouter>
<Header/>
<Route exact path='/' component={ProductList}/>
<Route exact path='/NotHome' component={NotHome}/>
</BrowserRouter>
);
}
//ProductList.js
const ProductList = (props) => {
const [Skip, setSkip] = useState(0);
const [Limit] = useState(5);
useEffect(() => {
props.requestProductList(Skip, Limit);
}, []);
return (
props.isLoading
? <h1>LOADING</h1>
: <div>
{props.productList && props.productList.map(product => (
<div key={product._id}>
<p>{product.productName}</p>
<img src={`http://localhost:5000${product.productMainPicture}`} style={{width: 200, height: 200}}/>
</div>
))}
</div>
)
};
const mapStateToProps = (state) => ({
productList: state.product.productList,
isLoading: state.product.isLoading,
});
const actionCreators = {
requestProductList
};
export default connect(mapStateToProps, actionCreators)(ProductList);
//ProductReducer.js
const PRODUCT_SET_PRODUCT_LIST = 'PRODUCT_SET_PRODUCT_LIST';
const SET_LOADING = 'SET_LOADING';
const initialState = {
productList: [],
isLoading: false
}
export const ProductReducer = (state = initialState, action) => {
switch (action.type) {
case PRODUCT_SET_PRODUCT_LIST:
return {
...state,
productList: [...action.productList],
productsCount: action.productsCount
}
case SET_LOADING:
return {
...state,
isLoading: action.isLoading
}
default:
return state;
}
};
const setProductList = (productList, productsCount) => ({type: PRODUCT_SET_PRODUCT_LIST, productList, productsCount});
const setLoading = (isLoading) => ({type: SET_LOADING, isLoading});
export const requestProductList = (skip, limit) => async (dispatch) => {
dispatch(setLoading(true));
try {
const res = await productApi.requestProductList(skip, limit);
dispatch(setProductList(res.data.products));
dispatch(setLoading(false));
} catch (e) {
console.log(e);
dispatch(setLoading(false));
}
};
//api.js
export const productApi = {
async requestProductList(skip, limit) {
return await Axios.post(`http://localhost:5000/api/product/get_product_list`, {skip, limit});
}
}
How about clearing the data when leaving the page and then when revisiting, everything should work as expected.
Let me explain,
Lets say you set the Redux state with ObjectA = {...}, then when you leave that page,
objectA still exists with values, so it immediately displays those values. While the network request is asynchronous and takes time to complete the promise and update the objectA.
To solve this, you can create a clearAction, which clears objectA when leaving the page.
useEffect(() =>
{
props.requestProductList(Skip, Limit);
return () =>
{
props.clearData()
}
}, []);
/* Redux reducers*/
case CLEAR_DATA:
return {...state, objectA: null}
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)))