I was under the impression that redux was synchronous but it does not seem to be in this case. I have this:
loadData() {
const filterData = {};
Object.values(this.props.filters).map(f => {
filterData[f.filter] = f.selectedValue || '';
});
this.props.fetchData({filters: filterData}); //redux call to load data
}
filterOnSubmit(filter, value, display) {
const newFilter = { filter, selectedValue: value, display };
this.props.updateFilter(newFilter);
this.loadData();
}
But the value of f does not include the selected value that is set BUT when I look at my redux devtools it is there. If I delay the mapping (or delay the loadData via putting it in a setState callback) it is there also.
action and reducer for reference:
case UPDATE_FILTER: {
const newFilter = { ...state[action.payload.filter], ...action.payload };
return { ...state, [action.payload.filter]: newFilter };
}
export function updateFilter(newFilterData) {
return {
type: UPDATE_FILTER,
payload: newFilterData
};
}
Although the redux store update is synchronous, the changes are not reflected in the same React cycle, the updates go though a re-render state, calling update lifecycle method.
After updating the store, you can call the actions that need to be called on store update in lifecycle method like componentDidUpdate (or componentWillReceiveProps which is now soon to be deprecated) or otherwise pass the filters to the loadData function directly
loadData(filters) {
const filterData = {};
Object.values(filters).map(f => {
filterData[f.filter] = f.selectedValue || '';
});
this.props.fetchData({filters: filterData}); //redux call to load data
}
filterOnSubmit(filter, value, display) {
const newFilter = { filter, selectedValue: value, display };
this.props.updateFilter(newFilter);
this.loadData(newFilter);
}
Using componentDidUpdate, you would write it like
componentDidUpdate(prevProps) {
// you have to perform an isEqual check since simply using '===' will not work for comparing arrays
if(!_.isEqual(prevProps.filters, this.props.filters)) {
this.loadData();
}
}
loadData() {
const filterData = {};
Object.values(this.props.filters).map(f => {
filterData[f.filter] = f.selectedValue || '';
});
this.props.fetchData({filters: filterData}); //redux call to load data
}
filterOnSubmit(filter, value, display) {
const newFilter = { filter, selectedValue: value, display };
this.props.updateFilter(newFilter);
}
You're on the right way, use componentDidUpdate to start a new fetch request. A thing you're possibly missing is an if-condition for changes of filter prop.
Docs remind about this:
You may call setState() immediately in componentDidUpdate() but note
that it must be wrapped in a condition like in the example above, or
you’ll cause an infinite loop.
And for your case it might be something like this, depending on the shape of filters:
componentDidUpdate(prevProps) {
if (this.props.filter !== prevProps.filter)
this.loadData();
}
Depending on code outside, you also might consider using a deep comparison inside if. But, of course, the best way would be to stay with === comparison and optimize mapStateToProps from outside (e.g. use meomization).
Do as SAmab said or move this
Object.values(this.props.filters).map(f => {
console.log(f);
filterData[f.filter] = f.selectedValue || '';
});
to your render function.
Related
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.
I'm very new to react and i'm confused why my state is not updated in another method of mine see example below.
fetchMovies = () => {
const self = this;
axios.get("https://api.themoviedb.org/3/trending/movie/day?api_key=XXXXXXX")
.then(function(response){
console.log(response.data)
self.setState({
collection: response.data.results
})
console.log(self.state.collection)
});
}
makeRow = () => {
console.log(this.state.collection.length);
if(this.state.collection.length !== 0) {
var movieRows = [];
this.state.collection.forEach(function (i) {
movieRows.push(<p>{i.id}</p>);
});
this.setState({
movieRow: movieRows
})
}
}
componentDidMount() {
this.fetchMovies();
this.makeRow();
}
When inside of fetchMovies function i can access collection and it has all the data but this is the part i can't understand in the makeRow function when i console log the state i would of expected the updated state to show here but it doesn't i'm even executing the functions in sequence.
Thanks in advance.
the collection is set after the async call is resolved. Even though makeRow method is called after fetchMoview, coz of async call, u will never know when the call will be resolved and collection state will be set.
There is no need to keep movieRows in the state as that is just needed for rendering. Keeping html mockup in the state is never a good idea.
So u should just call fetchMoviews in the componentDidMount and render the data in as follows:
render() {
const { collection } = this.state;
return (
<>
{
collection.map(c => <p>{c.id}</p>)
}
</>
)
}
make sure the initial value for collection in the state is [] .
The setState() documentation contains the following paragraph:
Think of setState() as a request rather than an immediate command
to update the component. For better perceived performance, React may
delay it, and then update several components in a single pass. React
does not guarantee that the state changes are applied immediately.
To access the modified state you need to use the function signature setState(updater, [callback]), so in your case it should be;
self.setState({
collection: response.data.results
}, () => { // Will be executed after state update
console.log(self.state.collection)
// Call your make row function here and remove it from componentDidMount if that is all it does.
self.makeRow()
} )
I have a component with some internal state (e.g. isLoading) which has access to redux data. In this component I'd like to dispatch a thunk action (api request) resulting in redux data change. After the thunk is completed I need to change the state of my component. As I see it, there are two ways to do so:
Use the promise return by the thunk and do all I need there, e.g.
handleSaveClick = (id) => {
const { onSave } = this.props;
this.setState({ isLoading: true });
onSave(id).then(() => this.setState({ isLoading: false }));
};
Pass a callback to the thunk and fire it from the thunk itself, e.g.
handleSaveClick = (id) => {
const { onSave } = this.props;
this.setState({ isLoading: true });
onSave(id, this.onSaveSuccess);
};
Which one is the correct way to do so?
The safer way is to use the promise implementation, as you'll be sure that function will only run after the promise has been resolved. The second implementation has no inherent flaws, but if anything in your thunk is async, then it will not work correctly since it'll run once the code is reached, not when the code above it finishes executing. When handling anything that can be async (server requests/loading data/submitting data), it's always safer to use Promise implementations.
Probably the best practice for updating component level state or running a callback function based on changing redux state (or any props/state changes for that matter) is to use componentDidUpdate or useEffect:
componentDidUpdate(prevProps, prevState){
if(prevProps.someReduxState !== this.props.someReduxState && this.state.isLoading){
setState({isLoading:false})
}
}
With useEffect:
useEffect(()=>{
if(!props.someReduxState){
setLoading(true)
} else {
setLoading(false)
}
},[props.someReduxState])
However, I might recommend a different approach (depending on the goal especially on initial data fetching) that manages the loading of state within redux:
Initialize your redux state with a loading value instead:
export default someReduxState = (state = {notLoaded:true}, action) => {
switch (action.type) {
case actions.FETCH_SOME_REDUX_STATE:
return action.payload;
default:
return state;
}
}
then in your component you can check:
if (this.props.someReduxState.notLoaded ){
// do something or return loading components
} else {
// do something else or return loaded components
}
I'm creating a react-native app and I need one of my components to use a axios get request when I do an action on another component. But the problem is that my component that I need an axios get request from is not being passed any props and the current state and new state is an array of 20+ objects with each at least 10 key value pairs. So I would need a component did update with a good if statement to not go into an infinite loop. I can't do an if statement with prevState to compare with current state because there is only a minor change happening in state. So I need to know how to stop the component Did Update from having an infinite loop.
state = {
favouriteData: []
}
componentDidMount () {
this.getFavouriteData()
}
componentDidUpdate (prevProps, prevState) {
if (this.state.favouriteData !== prevState.favouriteData){
this.getFavouriteData()
}
}
getFavouriteData = () => {
axios.get('http://5f46425d.ngrok.io')`enter code here`
.then(response => {
const data = response.data.filter(item => item.favourite === true)
this.setState({
favouriteData: data
})
})
}
The issue is that you are trying to compare 2 object references by doing the following. It will always return since the references are always different.
if (this.state.favouriteData !== prevState.favouriteData) {...}
To make life easier, we can use Lodash(_.isEqual) to deal with deep comparison of objects.
state = {
favouriteData: []
}
componentDidMount () {
this.getFavouriteData()
}
componentDidUpdate (prevProps, prevState) {
this.getFavouriteData(prevState.favouriteData)
}
getFavouriteData = (prevData) => {
axios.get('http://5f46425d.ngrok.io')
.then(response => {
const data = response.data.filter(item => item.favourite === true);
// compare favouriteData and make setState conditional
if (!prevState || !_.isEqual(prevData, data)) {
this.setState({
favouriteData: data
})
}
})
}
You should use react-redux to avoid this kind of issues. Assuming you are not using flux architecture, you can pass this.getFavouriteData() as props to the other component like:
<YourComponent triggerFavouriteData = {this.getFavouriteData}/>
As we've known, setState is async. I've read few questions about setState, on how to use the value right after setState, but those aren't what I need right now.
I'm trying to set value for array List, and then use that List to do a function to get the value for Result. If setState isn't async, then it would be like this
`
handleChange(e) {
const resultList = this.state.list.slice();
resultList[e.target.id] = e.target.value;
this.setState({
list: resultList,
result: this.doSomething(resultList) // this.doSomething(this.state.list)
});
}
`
Is there anyway to achieve this? A documentation or keyword to research would be awesome.
Many thanks
There is a callback parameter to setState which is called after the state has been updated
this.setState({
list: resultList,
result: this.doSomething(resultList)
}, () => {
//do something with the updated this.state
});
You can use async await like
async handleChange(e) {
const resultList = this.state.list.slice();
resultList[e.target.id] = e.target.value;
this.setState({
list: resultList,
result: await this.doSomething(resultList) // this.doSomething(this.state.list)
});
}
The use of this.state together with this.setState is discouraged, exactly because state updates are asynchronous and may result in race conditions.
In case updated state derives from previous state, state updater function should be used. Because it's asynchronous, event object should be treated beforehand, due to how synthetic events work in React:
const { id, value } = e.target;
this.setState(state => {
const list = [...state.list];
list[id] = value;
return {
list,
result: this.doSomething(list)
};
});