Cannot get the latest value of context in useEffect - reactjs

I am trying to use a context value as a initial state for a consuming component. I am subscribe to the state via useEffect but i am not getting the latest values.
i can avoid doing all these unnecessary things using componentWillMountwithin my provider but its going to be deprecated soon..
This is my context provider when mounted i am checking whether a token session is valid and changes the state accordingly which is being consume by some components.
state = {
userData: [],
isAuthenticated: false,
_defaultLogin: this._defaultLogin,
_setToken: this._setToken,
loginPopUp: false
}
componentDidMount(){
const { history } = this.props;
if(!this._validSession()){
this.setState({
loginPopUp: true,
isAuthenticated: false
})
} else {
try {
const userData = this._getUserData();
this.setState({
userData,
isAuthenticated: true
})
} catch (err) {
this._logOut();
history.push('/')
}
}
}
In one of the consuming component i want to know if some context values was updated and use the latest value.
const authService = useContext(AuthServiceContext) // use the context
const [modalShow, setModalShow] = useState(authService.loginPopUp) // use the context loginpopup value as initial state which is false by default
useEffect(() => {
setModalShow(authService.loginPopUp) // set the modal according to the value
}, authService.loginPopUp) // when provider changes state i am expecting to get the new value
I expect for the setModalShow() to use the latest value of authService.loginPopUp context but it is always false

Related

React-Redux: how to set the state?

I am trying to understand someone else their code but have difficulty understand the interaction between Redux and React.
On a React page, I invoke a Redux action called getSubscriptionPlan. Inside that Redux action, I see it is able to load the correct data (point 1 below). This uses a reducer, in which I can again confirm the correct data is there (point 2 below).
Then the logic returns to the React page (point 3 below). I now would expect to be able to find somewhere in the Redux store the previously mentioned data. However, I can't find that data listed anywhere... not in this.state (where I would expect it), nor in this.props. Did the reducer perhaps not update the store state...?
What am I doing wrong and how can I get the data to point 3 below?
React page:
import { connect } from "react-redux";
import { getSubscriptionPlan } from "../../../appRedux/actions/planAction";
async componentDidMount() {
let { planId } = this.state;
await this.props.getSubscriptionPlan(planId);
// 3. I can't find the data anywhere here: not inside this.state and not inside this.props.
this.setState({plan: this.state.plan});
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.payment.paymentData !== this.props.payment.paymentData) {
this.setState({
checkout: this.props.payment.paymentData,
plan: this.props.payment.paymentData.plan,
});
}
}
const mapStateToProps = (state) => {
return {
plan: state.plan,
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators(
{ getSubscriptionPlan }, dispatch
);
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Checkout)
);
Redux action:
export const getSubscriptionPlan = (id) => {
let token = getAuthToken();
return (dispatch) => {
axios
.get(`${url}/getSubscriptionPlan/${id}`, {
headers: { Authorization: `${token}` },
})
.then((res) => {
if (res.status === 200) {
// 1. From console.log(res.data) I know res.data correctly now contains the data
return dispatch({
type: GET_PLAN_SUCCESS,
payload: res.data,
});
})
};
};
Reducer:
export default function planReducer(state = initial_state, action) {
switch (action.type) {
case GET_PLAN_SUCCESS:
// 2. I know action.payload, at this point contains the correct data.
return { ...state, plan: action.payload };
default:
return state;
}
}
You are getting tripped up on how Redux works.
Redux does not use react component state. It manages state separately, and passes that state to components as props. When you call getSubscriptionPlan, you asynchronously dispatch an event to Redux, which handles the event and updates store state in the reducer. This state is the passed to the connected components mapStateToProps function, mapped to props, and then passed as props to your component. Passing new props triggers a componentDidUpdate and a rerender of the component.
A few key things here.
Redux does not interact with component state unless you explicitly set state with props passed from Redux.
Redux is asynchronous. That means that when you make a change to state via dispatch, the change is not immediately available in the component, but only available when new props are passed. It's event driven, not data binding. As a result, in your code you woun't see the plan prop in componentDidMount because at the time componentDidMount the call to getSubscriptionPlan hasn't happened.
You should see the prop populated in this.props in componentDidUpdate and in render before the didUpdate.
When working with react, it's best to think of components as basically functions of props with some extra lifecycle methods attached.

Recalculate initial state

Is there a way to recalculate the initial state for a slice. I am trying to reset the entire store doing:
const rootReducer = (state: any, action: any) => {
// Clear the state
if (action.type === clearState.type) {
// Clear all cached api requests
apiSlice.util.resetApiState();
// Return initial state
state = undefined;
}
return combinedReducer(state, action);
};
The problem is that my auth slice has the following initial state:
export const initialState = {
isLoggedIn: checkIfUserIsAlreadyLoggedIn(),
email: "",
status: "idle",
errorMessage: "",
};
So, when I start my application and checkIfUserIsAlreadyLoggedIn returns true, then when I reset the store, isLoggedIn is still true.
Is there any way I can recalculate the checkIfUserIsAlreadyLoggedIn function thus the initial auth slice state?
Thanks!
No. That would be a side effect within a reducer, which in Redux is strictly forbidden. A reducer should only depend on state and action, not on external data sources.
Also, apiSlice.util.resetApiState() will not do anything unless you dispatch it.
Maybe write it all as a thunk instead:
const resetAll = dispatch => {
dispatch(resetAction())
dispatch(apiSlice.util.resetApiState())
dispatch(setLoginState(checkIfUserIsAlreadyLoggedIn()))
}

Cannot update a component (xxx) while rendering a different component (yyy)

In my react app, I'm getting this strange error ("Cannot update a component (xxx) while rendering a different component (yyy)"). I understand what is causing the error, but I don't understand the "why" or how to fix it without restructuring a large portion of logic. So the components in the lifecycle and the underlying logic are as follows: "App" is the top level component, which contains state to an object called "grid". This state and its setter is passed down to a component called "Grid2". Grid2 also has its own state interfaced by a reducer (React.useReducer not React.useState). This reducer is passed the App State (and the grid obj inside of the state) as well as the setter to this state. So the reducer not only returns updated state for Grid2's state, but also may invoke the setter for App's state. React does not like this, but my only intuitive solution would be to move all of the logic that invokes the App's setter into useEffects which would be listening for changes on Grid2's state.
//--------------- App.tsx ---------------------
export const AppContext = React.createContext<AppContextType>({refs: initAppRefs, state: initAppState, setState: () => {}});
export function App() {
let { current: refs } = React.useRef<Refs>(initAppRefs);
const [state, setState] = React.useState<State>(initAppState);
return (
<AppContext.Provider value={{refs, state, setState}}>
<Home />
</AppContext.Provider>
);
}
//---------------- Grid2.tsx --------------------
import { AppContext, AppContextType, State } from "../App";
const gridStateReducer = (last: GridState, action: GridReducerAction): GridState => {
const newState: GridState = Helpers.deepCopy(last);
// centralized setter for tile.mouseDown, returns if change was made
const mouseDownOverride = (tile: string, value: boolean): boolean => {
// force tile to exist in newState.grid
if (!(tile in newState.grid)) {
newState.grid[tile] = {mouseDown: false, mouseOver: false};
}
// check to see if change is needed
if (newState.grid[tile].mouseDown !== value) {
newState.grid[tile].mouseDown = value;
// update appState grid fills
if (value) { //mousedown
if (tile in action.appState.grid) {
if (action.appState.currTool === "wall" && action.appState.grid[tile].fill === "empty") {
const newAppState: State = Helpers.deepCopy(action.appState);
newAppState.grid[tile].fill = "wall";
action.setAppState(newAppState);
}
}
}
return true;
} else {
return false;
}
}
if (action.type === GridReducerActionType.SetTileDown && action.data instanceof Array
&& typeof action.data[0] === "string" && typeof action.data[1] === "boolean") {
return mouseDownOverride(...(action.data as [string, boolean])) ? newState : last;
}
}
export const Grid2: React.FC<{}> = () => {
const { state: appState, setState: setAppState, refs: appRefs } = React.useContext<AppContextType>(AppContext);
const [gridState, gridStateDispatch] = React.useReducer(gridStateReducer, initGridState);
}
The code is a very selective set of logic from the actual project, as you may notice a lot of references seemingly appearing from nowhere, but I omitted this code as it just bloats the code and takes away from the logic path. So my question is, why does this happen (looking for an under-the-hood explanation), and how do I fix this without refactoring it too much?
By my estimation, the problem is probably due to side-effects in the gridStateReducer. The reducer functions passed to useReducer shouldn't have side-effects (i.e. call any setters or mutate any global state). The point of a reducer function is to take the current state, apply an action payload, and then return a new state, which will then prompt the React lifecycle to do whatever re-renders are necessary.
Since you're calling action.setAppState(newAppState) inside the reducer, and since that's a React state setter, my guess is that that's causing React to kick off a new render cycle before the reducer can finish. Since that new render cycle would cause components to update, it could then "cause a component to update (probably App) while rendering a different component (whatever is calling gridStateDispatch or invoking that reducer, probably Grid2)"
In terms of refactor, the requirement is that gridStateReducer return a new GridState and not cause any side-effects. First thing is probably to refactor the reducer to remove the side-effect and just return a new state:
const gridStateReducer = (last: GridState, action: GridReducerAction): GridState => {
const newState: GridState = Helpers.deepCopy(last);
// centralized setter for tile.mouseDown, returns if change was made
const mouseDownOverride = (tile: string, value: boolean): boolean => {
// force tile to exist in newState.grid
if (!(tile in newState.grid)) {
newState.grid[tile] = {mouseDown: false, mouseOver: false};
}
// check to see if change is needed
if (newState.grid[tile].mouseDown !== value) {
newState.grid[tile].mouseDown = value;
// update appState grid fills
return true;
} else {
return false;
}
}
if (action.type === GridReducerActionType.SetTileDown && action.data instanceof Array
&& typeof action.data[0] === "string" && typeof action.data[1] === "boolean") {
return mouseDownOverride(...(action.data as [string, boolean])) ? newState : last;
}
}
Now, it looks like that side-effect was interested in if (tile in action.appState.grid), so I'd need some way to have both tile and appState in context. Since I'm not sure what the structure is exactly, I'm assuming appState in the AppContext and action.appState are the same object. If not, then ignore everything after this sentence.
Looking at the reducer, it looks like we're passing the tile in as the first element in a tuple within the action passed to gridStateDispatch, so that means the caller of that function, which seems like Grid2, must know what tile should be at the time that the dispatch function is called. Since that component also has the AppContext in context, you should be able to do something like:
export const Grid2: React.FC<{}> = () => {
const { state: appState, setState: setAppState, refs: appRefs } = React.useContext<AppContextType>(AppContext);
const [gridState, gridStateDispatch] = React.useReducer(gridStateReducer, initGridState);
const handleSomethingWithTile = (tile: string, someBool: boolean) => {
gridStateDispatch({ type: GridReducerActionType.SetTileDown, data: [ tile, someBool ] })
if (tile in appState.grid) {
if (appState.currTool === "wall" && appState.grid[tile].fill === "empty") {
const newAppState: State = Helpers.deepCopy(appState);
newAppState.grid[tile].fill = "wall";
setAppState(newAppState);
}
}
}
}
This should be possible because the if (tile in appState.grid) statement doesn't seem to need the intermediate state value in the reducer, so it's possible to just move that decision out of the reducer scope here. This should prevent the sort of "state update in the middle of a state update" problem you have.
I should mention: I'd probably want to do some additional refactor here to help simplify the state logic. It seems like you're probably really close to wanting a tool like redux to help manage state here. Also should include a warning that passing full app state with setters via native React context like you're doing here can have pretty serious performance problems if you're not careful.

Updated state not reflecting in dispatch

I'm setting the state of contact then dispatching the state to the store but the state that is dispatched is empty while the state in my component is updated. This is in the callback too so not sure why it's not updated... am I missing something? Thanks everyone.
if (showForm === true) {
const {contact} = this.state;
this.setState({ contact: shipInfo, showForm: !showForm }, () => {
dispatch(updateContact(contact));
this.formValidation();
});
}
You are calling const { contact } = this.state, then using that version of contact to send to the store. But that is not the updated variable. It sounds like what you want to do is send
dispatch(updateContact(shipInfo));
which is what you're updating your variable to become in the next state.
Another way of writing would be:
this.setState({ contact: shipInfo, showForm: !showForm }, () => {
const { contact } = this.state;
dispatch(updateContact(contact));
this.formValidation();
});
Now you're grabbing the newer version of state, as you're defining it from within your callback, where state has already been updated.

fetching data after useReducer dispatch changes local state

Trying to rewrite this method using react hooks:
this.setState({
isPerformingAuthAction: true
}, () => {
auth.signInWithEmailAndPassword(emailAddress, password).then((value) => {
this.closeSignInDialog(() => {
const user = value.user;
const displayName = user.displayName;
const emailAddress = user.email;
this.openSnackbar(`Signed in as ${displayName || emailAddress}`);
});
}).catch((reason) => {
const code = reason.code;
const message = reason.message;
switch (code) {
case 'auth/invalid-email':
case 'auth/user-disabled':
case 'auth/user-not-found':
case 'auth/wrong-password':
this.openSnackbar(message);
return;
default:
this.openSnackbar(message);
return;
}
}).finally(() => {
this.setState({
isPerformingAuthAction: false
});
});
});
isPerformingAuthAction is a component property that disables buttons while performing various actions.
<Button disabled={state.isPerformingAuthAction} ... />
The Problem: I am using useReducer to manage local state and isPerformingAuthAction is a property of the reducerState. useReducer doesn't return a promise, so I can't do this:
dispatch({type:"isPerformingAuthAction", payload: true}).then(auth.stuff)
I thought about using useState instead, but it doesn't have a call back like this.setState with classes does.
I have been trying to wrap my head around some of the useEffect ways around fetching data and async. I am pretty confused about it, but I think I could have "isPerformingAuthAction" or the state from the reducer as a dependency and have useEffect update the component when it changes but the "isPerformingAuthAction" property changes for other reasons as well, not wtih just auth.signInWithEmailAndPassword(emailAddress, password), but signUp, signOut and a few others.
So when "isPerformingAuthAction" changes to true, it disables buttons so the user is forced to wait for the response from the server. I want to make sure those buttons are disabled (state updated/component re-rendered with greyed-out buttons) before I call the auth.whatever.

Resources