Prevent Redux from rerendering entire tree and clearing local state - javascript

For my app's signup form, I am maintaining the form values with local component state, but the currentUser state, error state, and the API calls are in Redux.
What I'd like to happen is when the form is submitted, the button has a loading spinner and the form values remain until a response is returned from the server. If the server responds with an authorized user, redirect to app. If there's an error, the form's values should not be cleared.
The problem seems to be that Redux is clearing my form values when it dispatches any state updating function (whether removing an error or making the API call to authorize the user). Is there any way to avoid this happening?
From my AuthForm.js
const submitData = () => {
setLoading(true);
if (formType === 'reset') {
updatePassword(resetToken, values.password, history)
.then(result => setLoading(false));
} else if (formType === 'forgot') {
forgotPassword(values.email, history);
} else {
console.log(values); // form values still populated
onAuth(formType, values, history)
.then(result => {
console.log('result received'); // values empty
setLoading(false);
if (formType === 'signup') {
history.push('/questionnaire')
} else {
history.push('/app')
}
})
.catch(err => setLoading(false));
}
};
From my redux actions.js file:
export function authUser(type, userData, history) {
return dispatch => {
dispatch(removeError());
console.log('dispatch') // by this time the form values are empty
// unless I comment out the dispatch(removeError()) above,
// in which case we still have values until 'token recevied' below
return apiCall('post', `/users/${type}`, userData)
.then(({ jwt, refresh_token, ...user }) => {
console.log('token received')
localStorage.setItem('jwtToken', jwt);
localStorage.setItem('jwtTokenRefresh', refresh_token);
dispatch(getUser(user.id));
})
.catch(err => {
handleError(dispatch, err);
});
};
}
I'm also logging the values in my AuthForm component. The result is this:
EDIT: It definitely looks like my components are unmounting but it's still not clear why to me, or how to prevent it.
I am trying to memoize the dispatch function but it seems to have no effect.
const dispatch = useDispatch();
const onAuth = useCallback(
(formType, values, history) => {
dispatch(authUser(formType, values, history))
},
[dispatch]
);

Your component is a subscriber to a redux store, thats why it re-renders,
component's state gets initialized to initial states,
Now you need, a way to persist these component states within rendering cycles.
On class component you would tie a value to a instance i.e using this.value == 'value'
so that it remains the same between renders,
On functional component to achieve this you make use of a hook called useRef
which doesnt change its value on re-render unless you manually change it,
Hopely it will help

Related

How to properly get when the redux dispatch is done, to then render a Component

I use react 18.2.0, redux 4.2.0 and redux thunk
I want to wait until my dispatch is done, so I can take the data it got from the server and render a component with that data
I try to do the following in my component:
import store from '../redux/store';
const featureInfoA = useSelector(featureInfo)
const featureErrorA = useSelector(featureError)
const featureStatusA = useSelector(featureStatus)
const clickFeatureFromList = (id) => {
//start dispatching
dispatch(fetchFeatureInfoById({ id, category }))
.then((e) => {
console.log('BINGO THEN ', featureInfoA, featureErrorA, featureStatusA);
})
}
//check store when a specific state changed and then get its data and render a component
store.subscribe(() => {
const state = store.getState()
if (state.features.status == 'succeeded' || state.features.status == 'failed') {
console.log('BINGO SUBSCRIBE ', featureInfoA);
accordionDetailsRootGlobal.render(<ContextInfo featureData={featureInfoA} />);
}
})
The issue is that in my console I see:
BINGO SUBSCRIBE a multiple times
and then the DATA from store that is inside the store
and then the BINGO THEN a null idle that is in the then after calling dispatch
I think that the log of the store should be first
Why I see "BINGO SUBSCRIBE a" that is inside the store.subscribe multiple times?
Why I never see the final data that came from the server? "BINGO THEN a null idle" contains the initial state, even though "DATA from store" brought back data.
By the way, this my store, for features
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
const FEATURE_URL = 'http://localhost:3500/map'
//status : idle, loading, succeeded, failed
const initialState = {
info:'a',
status:'idle',
error:null
}
export const fetchFeatureInfoById = createAsyncThunk('feature/fetchInfoById', (originalData) => {
console.log('fetchFeatureInfoById originalData ', originalData);
fetch(FEATURE_URL + '/feature/' + originalData.id + '/' + originalData.category)
.then((response) => response.json())
.then((data) => {
console.log('DATA from store ', data);
return data;
})
.catch((error) => {
return error.message;
});
})
export const featuresSlice = createSlice({
name: 'features',
initialState,
reducers: {
featureInfoById: {
reducer(state, action) {
//do something with the id
console.log('feature state ', ' -- state : ',state.status, ' -- action : ', action);
},
prepare(category, id) {
return{
payload:{
category,
id
}
}
}
}
},
extraReducers(builder) {
builder
.addCase(fetchFeatureInfoById.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchFeatureInfoById.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
.addCase(fetchFeatureInfoById.fulfilled, (state, action) => {
console.log('fulfilled data store ', state, action);
state.status = 'succeeded'
state.info = action.palyload
})
}
})
export const featureInfo = (state) => state.features.info;
export const featureError = (state) => state.features.error;
export const featureStatus = (state) => state.features.status;
export const {featureInfoById} = featuresSlice.actions
export default featuresSlice.reducer
I just want to get the data after the dispatch is done, to render a component with them. Please help me understand what I am doing wrong and how can I fix that.
Why I see "BINGO SUBSCRIBE a" that is inside the store.subscribe
multiple times?
The docs mention:
Adds a change listener. It will be called any time an action is
dispatched, and some part of the state tree may potentially have
changed.
So basically each time any action is dispatched and any part of the state tree might have been changed it will be triggered. This refers to your entire store (not just featuresSlice).
Another potential reason is that you are subscribing each time the component is mounted, but never unsubscribing (so you might have multiple subscriptions at the same time). If you really want to keep this, a fix would be to subscribe inside an useEffect unsubscribe on unmount (something like):
useEffect(() => {
const unsubscribeFn = store.subscribe(() => {...})
return unsubscribeFn
}
Why I never see the final data that came from the server? "BINGO THEN
a null idle" contains the initial state, even though "DATA from store"
brought back data.
This is most likely because your fetchFeatureInfoById is not returning anything (please note that the return data; part is inside a callback, but the main function doesn't return anything) so in your extra reducer action.payload has no value.
So what you want to do in order to be able to properly set the slice state from the slice extra reducer is returning the fetch result from the fetchFeatureInfoById function. So basically your function would look something like:
export const fetchFeatureInfoById = createAsyncThunk('feature/fetchInfoById', (originalData) => {
console.log('fetchFeatureInfoById originalData ', originalData);
// Add the return keyword before the fetch from the next line
return fetch(FEATURE_URL + '/feature/' + originalData.id + '/' + originalData.category)
.then((response) => response.json())
.then((data) => {
console.log('DATA from store ', data);
return data;
})
.catch((error) => {
return error.message;
});
})
PS: Without a reproducible example is not 100% this will fix all your problems, but it should at the very least point you in the right direction.
Extra suggestions:
(related to first answer): I think you can at least use an useEffect? It would be way more efficient to only trigger the an effect when accordionDetailsRootGlobal changes or featureInfoA (so basically have an useEffect with this 2 dependencies and render the ContextInfo when one of this 2 change, rather than rendering on every store update). Something like:
useEffect(() => {
if (featureInfoA)
accordionDetailsRootGlobal.render(<ContextInfo featureData={featureInfoA} />)
}, [accordionDetailsRootGlobal, featureInfoA])
inside fetchFeatureInfoById on the catch you don't throw any error. This will prevent .addCase(fetchFeatureInfoById.rejected ... from being called (since, even if your fetch fails, the function won't throw any error). So you should probably remove the catch part, or throw an error inside it.
You don't need to do the store.subscribe thing.
The selector is supposed to update and it will cause a render of your component. So, initially featureInfoA is undefined but after the promise is resolved it will update the store which will trigger a render in your component
import store from '../redux/store';
const featureInfoA = useSelector(featureInfo)
const featureErrorA = useSelector(featureError)
const featureStatusA = useSelector(featureStatus)
const clickFeatureFromList =(id) => {
//start dispatching
dispatch(fetchFeatureInfoById({id, category}))
}
// add some logic here to check if featureInfoA has value
if(featureErrorA) return <div>Error</div>
if(featureStatusA === "loading") return <div>Loading...</div>
if(featureInfoA) return <div>{featureInfoA}</div>
I Think this might be a better approach to deal with dispatch when you want to observe your data after you request successfully handled
const clickFeatureFromList =async (id) => {
//start dispatching
const result = await dispatch(fetchFeatureInfoById({ id, category }))
if (fetchFeatureInfoById.fulfilled.match(result)) {
const {payload} = result
console.log('BINGO THEN ', featureInfoA, featureErrorA, featureStatusA);
}
}

Firebase auth callback for user data updates - how to keep user data up do date in UI

bellow I have the standard way of setting the user state to currentuser that is returned from onAuthStateChanged. I would like a useEffect hook that is called when any update to user's info is made. I cant seam to figure out how to do that.
My current solution is to manually trigger a function that sets checkingStatus to true so that the bellow useEffect runs again.
useEffect(() => {
console.log("change")
const unsubscribe = onAuthStateChanged(auth, (currentuser) => {
setUser(currentuser);
setCheckingStatus(false);
});
return () => {
unsubscribe();
};
}, [aucheckingStatus]);
Is there a way to listen to any changes made to user and then trigger a useEffect that updates the user state ?
While auth.currentUser actually contains up to date user data, for reasons beyond my comprehension you cant actually trigger a useEffect using auth.currentUser because ReactJS is not recognising the change, also setting the user state to auth.currentUser will also not work, but for some reason if you spread auth.currentUser React will properly update state.
As a result my solution is to spread auth.currentUser after I finish a profile update or other user data related actions.
const updateUserProfile = (profileData) => {
updateProfile(auth.currentUser, profileData).then((res) => {
// Success
setUser({...auth.currentUser})
}).catch((error) => {
// Fail
})
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentuser) => {
setUser(currentuser);
});
return () => {
unsubscribe();
};
}, []);

Resetting State in React with saved initial State

I'm trying to save State twice, so I can reset it later on, but no matter what method I try, the 'setFullTrials' won't update with the saved data. The "console.log(savedData)" shows that all the data is there, so that's definitely not the problem. Not sure where I'm going wrong.
function AllTrials({Trialsprop}) {
let [savedData, setSavedData] = useState([]);
let [fullTrials, setFullTrials] = useState([]);
useEffect(() => {
//Call the Database (GET)
fetch("/trials")
.then(res => res.json())
.then(json => {
// upon success, update trials
console.log(json);
setFullTrials(json);
setSavedData(json);
})
.catch(error => {
// upon failure, show error message
});
}, []);
const resetState = () => {
setFullTrials(savedData);
//setFullTrials((state) => ({
...state,
savedData
}), console.log(fullTrials));
// setFullTrials(savedData.map(e => e));
console.log("savedData", savedData)
}
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, it will likely run before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(fullTrials)
// Whatever else we want to do after the state has been updated.
}, [fullTrials])
This console.log will run only after the state has finished changing and a render has occurred.
Note: "fullTrials" in the example is interchangeable with whatever other state piece you're dealing with.
Check the documentation for more info.
P.S: the correct syntax for useState is with const, not let.
i.e. - const [state, setState] = useState()

Setting a useEffect hook's dependency within`useEffect` without triggering useEffect

Edit: It just occurred to me that there's likely no need to reset the variable within the useEffect hook. In fact, stateTheCausesUseEffectToBeInvoked's actual value is likely inconsequential. It is, for all intents and purposes, simply a way of triggering useEffect.
Let's say I have a functional React component whose state I initialize using the useEffect hook. I make a call to a service. I retrieve some data. I commit that data to state. Cool. Now, let's say I, at a later time, interact with the same service, except that this time, rather than simply retrieving a list of results, I CREATE or DELETE a single result item, thus modifying the entire result set. I now wish to retrieve an updated copy of the list of data I retrieved earlier. At this point, I'd like to again trigger the useEffect hook I used to initialize my component's state, because I want to re-render the list, this time accounting for the newly-created result item.
​
const myComponent = () => {
const [items, setItems] = ([])
useEffect(() => {
const getSomeData = async () => {
try {
const response = await callToSomeService()
setItems(response.data)
setStateThatCausesUseEffectToBeInvoked(false)
} catch (error) {
// Handle error
console.log(error)
}
}
}, [stateThatCausesUseEffectToBeInvoked])
const createNewItem = async () => {
try {
const response = await callToSomeService()
setStateThatCausesUseEffectToBeInvoked(true)
} catch (error) {
// Handle error
console.log(error)
}
}
}
​
I hope the above makes sense.
​
The thing is that I want to reset stateThatCausesUseEffectToBeInvoked to false WITHOUT forcing a re-render. (Currently, I end up calling the service twice--once for win stateThatCausesUseEffectToBeInvoked is set to true then again when it is reset to false within the context of the useEffect hook. This variable exists solely for the purpose of triggering useEffect and sparing me the need to elsewhere make the selfsame service request that I make within useEffect.
​
Does anyone know how this might be accomplished?
There are a few things you could do to achieve a behavior similar to what you described:
Change stateThatCausesUseEffectToBeInvoked to a number
If you change stateThatCausesUseEffectToBeInvoked to a number, you don't need to reset it after use and can just keep incrementing it to trigger the effect.
useEffect(() => {
// ...
}, [stateThatCausesUseEffectToBeInvoked]);
setStateThatCausesUseEffectToBeInvoked(n => n+1); // Trigger useEffect
Add a condition to the useEffect
Instead of actually changing any logic outside, you could just adjust your useEffect-body to only run if stateThatCausesUseEffectToBeInvoked is true.
This will still trigger the useEffect but jump right out and not cause any unnecessary requests or rerenders.
useEffect(() => {
if (stateThatCausesUseEffectToBeInvoked === true) {
// ...
}
}, [stateThatCausesUseEffectToBeInvoked]);
Assuming that 1) by const [items, setItems] = ([]) you mean const [items, setItems] = useState([]), and 2) that you simply want to reflect the latest data after a call to the API:
When the state of the component is updated, it re-renders on it's own. No need for stateThatCausesUseEffectToBeInvoked:
const myComponent = () => {
const [ items, setItems ] = useState( [] )
const getSomeData = async () => {
try {
const response = await callToSomeService1()
// When response (data) is received, state is updated (setItems)
// When state is updated, the component re-renders on its own
setItems( response.data )
} catch ( error ) {
console.log( error )
}
}
useEffect( () => {
// Call the GET function once ititially, to populate the state (items)
getSomeData()
// use [] to run this only on component mount (initially)
}, [] )
const createNewItem = async () => {
try {
const response = await callToSomeService2()
// Call the POST function to create the item
// When response is received (e.g. is OK), call the GET function
// to ask for all items again.
getSomeData()
} catch ( error ) {
console.log( error )
}
} }
However, instead of getting all items after every action, you could change your array locally, so if the create (POST) response.data is the newly created item, you can add it to items (create a new array that includes it).

Infinite loop when dispatching in componentWillMount

I'm working in a React + Redux + redux-thunk codebase and I'm seeing some odd behavior. If I attempt to execute TWO actions in componentWillMount, the second action will infinitely loop.
Here's the componentWillMount:
componentWillMount() {
const { actions } = this.props;
// Action #1 (synchronous)
actions.openLoader();
// Action #2 (promise-based fetch)
actions.getListingsPurchased().then(() => {
actions.closeLoader();
})
.catch(error => {
console.error(error);
});
}
The first action, openLoader() is a simple state update. The second action does a fetch to the server. Action file here:
export function openLoader() {
return {
type: TYPES.SET_LOADER_OPEN
};
}
export function getListingsPurchased() {
return dispatch => {
return fetch'URL GOES HERE', { 'credentials': 'include' })
.then(response => {
return response.json();
})
.then(response => {
return dispatch({ type: TYPES.SET_LISTINGS, data: response.data });
});
};
}
If I was to remove the first action openLoader() from componentWillMount the infinite loop does not happen. Otherwise the fetch call will keep repeating endlessly.
Any help would be appreciated, I seem to have hit a wall.
I believe the best place for breaking infinite loop is in Redux reducer. Reducer is the place where you have to decide if you going to update the state of your app -> will trigger re-render of your components -> will trigger fetch action.
So try to put in place some reducer condition where you can recognize that state was already fetched before and you not going to update the state.

Categories