I'm currently trying to convert some hooks over to being inside class components in React. I'm attempting to recreate the code that allows for session storage so that the app doesn't need to fetch from the API so much. Here is the code:
useEffect(() => {
if(sessionStorage.homeState){
// console.log("Getting from session storage");
setState(JSON.parse(sessionStorage.homeState));
setLoading(false);
}else{
// console.log("Getting from API");
fetchMovies(POPULAR_BASE_URL);
}
}, [])
useEffect(() => {
if(!searchTerm){
// console.log("Writing to session storage");
sessionStorage.setItem('homeState', JSON.stringify(state));
}
}, [searchTerm, state])
These are two useEffect hooks so it makes sense to me to go with a regular componentDidMount to get from the session storage. The only thing that I can't seem to figure out is how to recreate the second useEffect that sets session storage and fires only when searchTerm or state changes. searchTerm and state are simply two properties of the state. How could I implement this since componentDidMount only fires once when the app first mounts? Thanks
The only thing that I can't seem to figure out is how to recreate the second useEffect that sets session storage and fires only when searchTerm or state changes. searchTerm and state are simply two properties of the state.
componentDidMount() is only one of methods used by the class components you can recreate the second hook with componentWillUpdate() or shouldComponentUpdate().
For example:
componentWillUpdate(nextProps, nextState) {
if (this.props.searchTerm !== prevProps.searchTerm) {
...
}
}
You can check what lifecycle methods are available in class components by Googling "class component lifecycle".
But as you can read in the comment to your questions Hooks can offer you more than class components, and recreating them is not trivial. It is easier to move from the class component to the Hooks.
Related
I am trying to use react hooks to make a Table component that displays rows of data from an API based on a set of filters that the user can choose. I want to make a new call to fetch data whenever the user clicks an 'Apply Filters' button, not when the user makes changes to the filters.
I am using context to manage the 'filters' state and a 'lastFetched' state which tracks when the user last clicked the 'Apply Filters' button (as well as other states on the page). Updates to the context are made via the useReducer hook and its dispatch method (see here).
The data fetching occurs in a useEffect hook that reruns whenever the 'lastFetched' state changes. This appears to be working correctly; however, the effect references other values from the context (i.e. the filters) that are not included in the dependencies. I am aware of the exhaustive-deps eslint rule, and I am concerned that I am not handling the hook's dependencies correctly.
const Table = () => {
const [context, dispatch] = useTableContext(); // implemented with createContext and useReducer
const { filters, lastFetched } = context;
useEffect(() => {
if (!filters.run) {
return;
}
dispatch({ type: 'FETCH_DATA_BEGIN' });
const params = convertContextToParams(context); // this is lazy, but essentially just uses the the filters and some other state from the context
API.fetchData(params)
.then((data) => {
dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data.results });
})
.catch((e) => {
dispatch({ type: 'FETCH_DATA_FAILURE', payload: e.response.data.message });
});
return () => { ... some cleanup... };
}, [lastFetched]); // <== This is the part in question
return <...some jsx.../>
};
Again, this appears to be working, but according to the react docs, it seems I should be including all the values from the context used in the hook in the hook's dependencies in order to prevent stale references. This would cause the logic to break, since I don't want to fetch data whenever the filters change.
My question is: when the user clicks 'Apply Filters', updates context.lastFetched, and triggers the useEffect hook, will the hook be referencing stale filter state from the context? If so, why? Since the effect is rerun whenever the button is clicked, and all the state updates are done via a reducer, does the usual danger of referencing stale variables in a closure still apply?
Any guidance appreciated!
Note: I have thought about using useRef to prevent this issue, or perhaps devising some custom async middleware to fetch data on certain dispatches, but this is the solution I currently have.
I am not an expert but I would like to provide my takes. According to my understanding of how Context works, you will not get stale filter data with the current implementation. useReducer updates the state with a new object which will trigger Table to be re-render.
Also, Table component doesn't really care about filter data unless lastFetched is changed by a click event. If lastFetched is changed, all the Consumer of TableContext will be re-render again. You should not get stale filter data either.
I am currently working on a simple React app with a very common workflow where users trigger Redux actions that, in turn, request data from an API. But since I would like to make the results of these actions persistent in the URL, I have opted for React Router v4 to help me with the job.
I have gone through the Redux integration notes in the React Router documentation but the idea of passing the history object to Redux actions just doesn't feel like the most elegant pattern to me. Since both Redux and Router state changes cause React components to be re-rendered, I'm a little worried the component updates could go a bit out of control in this scenario.
So in order to make the re-rendering a bit more predictable and sequential, I have come up with the following pattern that attempts to follow the single direction data flow principle:
Where I used to trigger Redux actions as a result of users' interactions with the UI, I am now calling React Router's props.history.push to update the URL instead. The actual change is about updating a URL parameter rather than the whole route but that's probably not that relevant here.
Before:
// UserSelector.jsx
handleUserChange = ({ target: selectElement }) => {
// Some preliminary checks here...
const userId = selectElement.value
// Fire a Redux action
this.props.setUser(userId)
}
After:
// UserSelector.jsx
handleUserChange = ({ target: selectElement }) => {
// Some preliminary checks here...
const userId = selectElement.value
// Use React Router to update the URL
this.props.history.push(`/user-selector/${userId}`)
}
The userId change in the URL causes React Router to trigger a re-render of the current route.
Route definition in App.jsx:
<Route path="/user-selector/:userId?" component={UserSelector} />
During that re-render, a componentDidUpdate lifecycle hook gets invoked. In there I am comparing the previous and current values of the URL parameter via the React Router's props.match.params object. If a change is detected, a Redux action gets fired to fetch new data.
Modified UserSelector.jsx:
componentDidUpdate (prevProps) {
const { match: { params: { userId: prevUserId } } } = prevProps
const { match: { params: { userId } } } = this.props
if (prevUserId === userId) {
return
}
// Fire a Redux action (previously this sat in the onChange handler)
this.props.setUser(userId)
}
When the results are ready, all React components subscribed to Redux get re-rendered.
And this is my attempt to visualise how the code's been structured:
If anyone could verify if this pattern is acceptable, I would be really grateful.
For step 3, I suggest a different approach which should be more in line with react-router:
react-router renders a component based on a route
this component should act as the handler based on the particular route it matches (think of this as a container or page component)
when this component is mounted, you can use componentWillMount to fetch (or isomorphic-fetch) to load up the data for itself/children
this way, you do not need to use componentDidUpdate to check the URL/params
Don't forget to use componentWillUnmount to cancel the fetch request so that it doesn't cause an action to trigger in your redux state
Don't use the App level itself to do the data fetching, it needs to be done at the page/container level
From the updated code provided in the question:
I suggest moving the logic out, as you would most likely need the same logic for componentDidMount (such as the case when you first hit that route, componentDidUpdate will only trigger on subsequent changes, not the first render)
I think it's worth considering whether you need to store information about which user is selected in your Redux store and as part of URL - do you gain anything by structuring the application like this? If you do, is it worth the added complexity?
I want to create an app with react and redux. My component subscribed to several states from the redux store, some of the state-data need to be prepared before the rendering can take place. Do I need to put the prepareData function into componentWillReceiveProps and write it to the state afterwards? It seems to create a lot of queries in the componentWillReceiveProps. Is there a best practice?
componentWillReceiveProps(nextProps) {
if (this.props.dataUser !== nextProps.dataUser) {
this.prepareData(nextProps.dataUser);
}
if (this.props.dataProject !== nextProps.dataProject) {
.....
}
if (this.props.dataTasks !== nextProps.dataTasks) {
.....
}
}
As Axnyff suggests, you can do your data preparation in mapStateToProps, this will trigger a render each time your redux state updates (your component can be stateless this way) :
mapStateToProps = (state) => {
const dataUserPrepared = prepareData(state.dataUser);
return { dataUser: dataUserPrepared };
}
If you have a lot of different data to prepare, which updates individually, that can be a loss in performance.
In this case you can use componentWillReceiveProps like in your question, this is fine because the setState in your prepareData() function will be batched with the received props to trigger only one render per prop update.
If you were using an app without redux then the solution would be to prepare your data before you call this.setState().
I believe the same solution applies to when using redux, your can prepare your data inside your action because you return the action object having a type and payload.
You can also prepare your data inside your reducer before returning the state object.
You could even prepare your data inside mapStateToProps of your component.
But in case you want to specific conditions under which component should re-render when state changes, then you do that in shouldComponentUpdate()
I have a React Component with a toggle on it (on or off). The on / off state is handled by the components own state (this.state).
But I want that state to remain when the user goes from one page to the next. For instance they are on home.html and then when user clicks to another page like about.html.
Also this is not a single page app. Do I want Redux or Mobox or some other state management tool? Suggestions are welcomed.
But I want that state to remain when the user goes from one page to the next.
As has been said in comments probably the most straight-forward way is to just store the state to localstorage and retrieve it when the component mounts.
class Toggle extends Component {
componentDidMount() {
const storedValue = localStorage.getItem("my_value");
if (storedValue) {
this.setState({ value: storedValue });
}
}
handleChange = e => {
const value = e.target.value;
this.setState({ value });
localStorage.setItem("my_value", value);
}
render() {
return ...
}
}
Also this is not a single page app. Do I want Redux or Mobox or some other state management tool? Suggestions are welcomed.
No, Redux and Mobx aren't necessary, they are state containers that have ways to persist to localstorage (for example redux-localstorage and mobx-localstorage), but the key is just persisting to localstorage.
If you are not moving pages (whole page refresh) and only using different components then you can simply define a state in parent component and pass it in the child components along with a function that would toggle the state.
Function would look like this:
ToggleState = newState => this.setState({ myState : newState });
Pass this function as prop to child component.
Use it in child component as
This.props.toggle(newState);
**but if it is across multiple pages the you can go for localstorage **
Hope this resolves your issue.
After completing the guide on the Alt site I am confused if Alt uses Reacts state to set properties? In part 3 - using Stores, it says
Instance variables defined anywhere in the store will become the
state.
It then puts a variable in the store:
this.locations = [];
Yet if I go into the main React component and log the state, after locations is given data of course, I get undefined except on the props?
loggState() {
console.log(this.state); // undefined
console.log(this.locations); // undefined
console.log(this.props.locations); // [object][object]..
}
Can anyone explain the relationship between states and props when using Alt?
In Alt—and indeed most Flux implementations—the store is a totally different part of your application to your components.
A component subscribes to changes in a store, then uses the changed data to update its local state, causing it to re-render.
We derive the initial state of components that use the LocationStore from whatever the store's state is when we instantiate the component.
getInitialState() {
return LocationStore.getState();
},
Then we set up a subscription, listening for changes to the store.
componentDidMount() {
LocationStore.listen(this.onChange);
},
And finally, we use the subscription handler to apply these changes to the component's local state. The way you decide to apply each update is totally up to you.
onChange(state) {
this.setState(state);
},
Each time we call this.setState with a new state, the component will re-render.
You could also use a higher-order component to subscribe to the store, then covert the store's state to props and pass them down to a wrapped component, instead. In which case, your component wouldn't need to be stateful at all.