Props change (redux) resets the local state when using useState - updated - reactjs

I am rewriting my class components into functional components. One component uses both Redux and local-state and I noticed that every time the props (redux state) change, the useState hook is executed, resetting my local state. Is this expected behavior, and how to persist your local-state?
old (simplified)
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
new
// Count gets reset to 0 every time reduxState changes
const MyComponent = ({reduxState}) => {
const [count, setCount] = useState(0)
UPDATE - the cause of the problem is not in the refactoring into functional components. I have a parent (Higher Order Component) that re-creates the wrapped component on changes to Redux state. For clarity, here is the somewhat simplified HOC
import React, {useEffect} from 'react'
import { connect } from 'react-redux'
import { fetchObject } from '../actions/objects'
import { fetchIfNeeded } from '../utils'
const withObject = ({element}) => WrappedComponent => ({id, ...rest}) => {
const Fetcher = ({status, object, fetchObject}) => {
useEffect(() => {
fetchIfNeeded(
status,
()=>fetchObject(element, id),
)
})
// THIS SECTION IS THE CAUSE OF MY PROBLEM
// Handle loading and error status
if (status && status.error) return <>error loading: {status.error.message}</>
if (status === undefined || object === undefined || status.isFetching) return <>loading</>
// END OF SECTION
// Pass through the id for immediate access
return <WrappedComponent {...{object, status, id}} {...rest} />
}
const mapStateToProps = state => ({
object: state.data[element][id],
status: state.objects[element][id]
})
const mapDispatchToProps = {
fetchObject
}
const WithConnect = connect(mapStateToProps, mapDispatchToProps)(Fetcher)
return <WithConnect/>
}
export default withObject
UPDATE2: The code sample above has been updated to highlight the problematic section. Every time the status changes (e.g. isFetching: true), the hoc returned a different component to reflect this. When it changes again to isFetching: false, a new instance of wrapped component is created and returned, losing its original state.
In short, it is a bad idea to return different components from a HOC depending on redux state. This should be handled in the regular component.

Related

Use multiple 'useContext' in one React component

What is a good, or even conventional, way to use the multiple 'useContext' hooks in one React component. Usually I am pulling the state and dispatch from the provider like so:
const { state, dispatch } = useContext(thisIsTheContext);
Of course this means that I define the state and dispatch in the context itself.
Now I've read about some people making a sort of 'rootContext' where you can pass all state trough one Provider. However, this seems a little overkill to me.
Of course I can name state amd dispatch differently, however, I think it is the convention to just use these two when making use of the useReducer hook.
Anyone that could shed some light on this?
EDIT (as requested how the App.js component looks like):
function App() {
return (
<FlightDataProvider>
<TravellerProvider>
<Component /> // component here
</TravellerProvider>
</FlightDataProvider>
);
}
I think there is no need for rootContext. What I do is I define useReducer inside the specific Context Provider. I provide state and functions for a specific context like below.
FlightDataProvider.js
import React, { useReducer, createContext } from 'react'
const flightDataReducer = (state, action) => {
switch (action.type) {
case 'SET_FLIGHT_DATA':
return {
...state,
flightData: action.payload,
}
default:
return state
}
}
export const FlightDataContext = createContext();
export const FlightDataProvider = props => {
const initialState = {
flightData: 'flightData'
}
const [state, dispatch] = useReducer(flightDataReducer, initialState)
const setFlightData = newFlightData => {
dispatch({ type: 'SET_FLIGHT_DATA', payload: newFlightData })
}
return (
<FlightDataContext.Provider
value={{
flightData: state.flightData,
setFlightData
}}>
{props.children}
</FlightDataContext.Provider>
)
}
After that, if I want to subscribe to two different context in the same component, I do like this;
SomeComponent.js
import React from 'react'
import { FlightDataContext } from '...'
import { AnotherContext } from '...'
export const SomeComponent = () => {
const {
flightData,
setFlightData
} = useContext(FlightDataContext)
const {
someValue
setSomeValue
} = useContext(AnotherContext)
return (...)
}
PS you might want to separate flightDataReducer function, move it in another js file and import in inside FlightDataProvider.js
If I understand your question, you are concerned about how to overcome name clashes when pulling in multiple contexts in one react component since in their original files they are all objects having the same property 'despatch'.
You can use an aspect of es6 destructuring to rename the diff context object properties right inside your component.
Like this:
const { user, despatch: setUser } = useContext(UserContext);
const { theme, despatch: setTheme } = useContext(ThemeContext);
const { state, despatch: setState } = useReducer(reducer);
I chose the names setUser, setTheme, and setState arbitrarily. You can use any name you like.

Should I use ComponentDidMount or mergeProps of connect function for data fetching?

I use react with redux and have a component which displays some dataSet fetched from external source. My current code looks like:
const ConnectedComponent = connect(
state => ({
dataSet: state.dataSet
}),
dispatch => ({
loadData: () => {
...
fetch data and dispatch it to the store
...
}
})
)(MyComponent);
class MyComponent extends Component {
...
componentDidMount() {
const { dataSet, loadData } = this.props;
if (!dataSet) {
loadData();
}
}
...
render () {
const { dataSet } = this.props;
if (dataSet) {
// render grid with data
} else {
// render Loading...
}
}
}
The code above works but I'm wondering would it be better to get rid of componentDidMount and just check for data and load it from within connect function? The code might looks like:
const ConnectedComponent = connect(
state => ({
dataSet: state.dataSet
}),
dispatch => ({
dispatch
}),
(stateProps, dispatchProps) => {
const { dataSet } = stateProps;
const { dispatch } = dispatchProps;
if (!dataSet) {
// fetch data asynchronously and dispatch it to the store
}
return {
...stateProps
};
}
)(MyComponent);
class MyComponent extends Component {
render () {
const { dataSet } = this.props;
if (dataSet) {
// render grid with data
} else {
// render Loading...
}
}
}
The latter code looks more attractive to me because of MyComponent becomes simpler. There is no passing of execution first forward from connected component to presentational and then backward, when componentDidMount detects that there are no data ready to display.
Are there some drawbacks of such approach?
PS: I use redux-thunk for an asynchronous fetching.
The second approach, as separation of concepts, is potentially a good solution, because of the layers and responsibility separations - ConnectedComponent is responsible for data fetching, while MyComponent acts as presentational component. Good!
But, dispatching actions in connect mergeProps doesn't seem a good idea, because you introduce side effects.
Also, other drawback I'm seeing, is that the flow of fetching and returning data would be repeated across your different pages (components). Generally speaking the following flow would be repeated:
Connected components call the API for the needed Entities.
While fetching the Entities, we’re showing a Loader.
When the data is available, we pass it to the Presentational components.
Because of the above drawbacks, I can suggest you to organize and reuse your data fetching flow in a HOC.
Here's a pseudo code and flow (taken from my article) that addresses the above drawbacks:
* I've been using it last 1 year and continue stick with it.
Fetcher HOC:
import authorActions from 'actions/author'
const actions = {
'Author': authorActions
}
export default (WrappedComponent, entities) => {
class Fetcher extends React.Component {
// #1. Calls the API for the needed Entities.
componentDidMount () {
this.fetch()
}
fetch () {
const { dispatch } = this.props
entities.forEach(name => dispatch(actions[name].get()))
}
render () {
const { isFetching } = this.props
// #2. While fetching the Entities, we're showing an Loader.
if (isFetching) return <Loader />
return <WrappedComponent {...this.props} />
}
}
const mapStateToProps = state => {
const isFetching = entities
.map(entity => state[entity].isFetching)
.filter(isFetching => isFetching)
return { isFetching: isFetching.length > 0 }
}
return connect(mapStateToProps)(Fetcher)
}
Usage:
const MyComponent = ({ authors }) => <AuthorsList authors={authors} />
const mapStateToProps = state => ({
authors: state.authors
})
const Component = connect(mapStateToProps)(MyComponent)
export default Fetcher(Component, ['Author'])
Here you can read the article and deep dive into its ideas and concepts:
* Fetcher concept is addressed at Lesson #2: Containers on steroids
Long-term React & Redux SPA — Lessons learned

How and where to dispatch action when react router changes props

I may be missing something here, change in react-router params using Link will cause the props to change with the new param(s) and trigger an update cycle.
You can't dispatch an action in the update cycle but that's the only place where the parameter is available. I need to get new data when the day parameter changes in my stateless component.
The component has 2 links, previous and next day. The previous day looks like this:
<Link to={"/sessions/" + prefDay}>{prefDay}</Link>
[UPDATE]
Here is the solution so far:
The Overview component is just a function taking props and returning jsx, the following is the container that will check if date is set and if it's not it'll redirect. If date is set then it'll return Overview.
It also checks if the router day paramater changed, if it did then it'll set dispatchFetch to true.
This will then cause the render function to asynchronously dispatch the getData action.
Not sure if there would be another way to do this, I would prefer to listen to events from router and dispatch the events from there but there is no (working) way to listen to the router.
import { connect } from 'react-redux';
import React, { Component } from 'react';
import Overview from '../components/Overview';
import { getData } from '../actions';
import { selectOverview } from "../selectors";
import { Redirect } from "react-router-dom";
const defaultDate = "2018-01-02";
const mapStateToProps = (lastProps => (state, ownProps) => {
var dispatchFetch = false;
if (lastProps.match.params.day !== ownProps.match.params.day) {
lastProps.match.params.day = ownProps.match.params.day;
dispatchFetch = true;
}
return {
...selectOverview(state),
routeDay: ownProps.match.params.day,
dispatchFetch
}
})({ match: { params: {} } });
const mapDispatchToProps = {
getData
};
class RedirectWithDefault extends Component {
render() {
//I would try to listen to route change and then dispatch getData when
// routeDay changes but none of the methods worked
// https://github.com/ReactTraining/react-router/issues/3554
// onChange in FlexkidsApp.js never gets called and would probably
// result in the same problem of dispatching an event in update cycle
// browserHistory does not exist in react-router-dom
// this.props.history.listen didn't do anything when trying it in the constructor
//So how does one dispatch an event when props change in stateless components?
// can try to dispatch previous and next day instead of using Link component
// but that would not update the url in the location bar
if (this.props.dispatchFetch) {
Promise.resolve().then(() => this.props.getData(this.props.routeDay));
}
return (this.props.routeDay)//check if url has a date (mapStateToProps would set this)
? Overview(this.props)
: <Redirect//no date, redirect
to={{
pathname: "/list/" + defaultDate
}}
/>
}
}
export default connect(mapStateToProps, mapDispatchToProps)(RedirectWithDefault);
Intially I expect your Router is like,
<Route path="/sessions/:prefDay" component={MyComponent}/>
You will have to do this in getDerivedStateFromProps(),
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.prefDay !== nextProps.match.params.prefDay) {
return {
prefDay: nextProps.match.params.prefDay,
};
}
// Return null to indicate no change to state.
return null;
}
Have you tried componentWillReceiveProps?
componentWillReceiveProps(props){
if(this.props.data != props.data && this.props.state.routeDay){
this.props.getData(props.state.routeDay);
}
}
To add parameter to route you need in router do this
<Route
path="/sessions/:prefDay"
....
/>
It means that now "/sessions/" rout have a parameter named "prefDay"
Example /sessions/:prefDay
and now in Link component need to add this parameter
<Link to={"/sessions/" + prefDay}>{prefDay}</Link>
You can get this value from url like this
this.props.match.params.prefDay
In RedirectWithDefaults which is a container of the stateless component I added componentDidMount (not an update cycle method):
const historyListener = (lastDay => (props,currentDay) => {
if(lastDay !== currentDay){
lastDay = currentDay;
if(!isNaN(new Date(currentDay))){
console.log("getting:",currentDay);
props.getData(currentDay);
}
}
})(undefined);
class RedirectWithDefault extends Component {
componentDidMount(history) {
this.props.getData(this.props.routeDay || defaultDate);
this.unListen = this.props.history.listen((location) => {
historyListener(
this.props,
location.pathname.split('/').slice(-1)[0]
);
});
}
componentWillUnmount(){
this.unListen();
}
Removed code from the render function. Now when it mounts it'll dispatch the action to load data and when the route changes it will dispatch it again but not in the update cycle.

Best practice for making small changes in UI with React and Redux

I'm using Redux and React to load data from a web service which is working well. I'd like to make small non-webservice-based changes in the UI in response to an action. A simplified example:
class SmartComponent extends React.Component {
handleClick = (e) => {
// how to best handle a simple state change here?
}
render() {
const { displayMessage } = this.props
return (
<DumbComponent message={displayMessage}/>
<button onclick={this.handleClick}>Change Message</button>)
}
}
const mapStateToProps = state => {
// state variables linked in the reducer
}
export default connect(mapStateToProps)(SmartComponent)
let DumbComponent = ({ message }) => {
return ({message})
}
If I modify the state in SmartComponent, for instance, by using this.setState, the props of SmartComponent will not be automatically updated. I believe it's a React anti-pattern to directly modify the props of SmartComponent. Is the best way to update the message in DumbComponent to make an action creator and link it in the reducer? That seems a bit overkill for a simple message change.
Yes, you should link it to the reducer.
However this is not mandatory:
How to do it
One other way to do this would be to store the message in the state of the SmartComponent.
Beware about the fact that Redux is no longer the single source of truth for the message.
class SmartComponent extends React.Component {
constructor(props) {
super(props)
// Initialize state based on props
this.state = {
message: props.message,
}
}
componentWillReceiveProps(nextProps) {
// Handle state update on props (ie. store) update
this.setState({ message: ... })
}
handleClick = (e) => {
this.setState({ message: ... })
}
render() {
const { displayMessage } = this.state
return (
<DumbComponent message={displayMessage}/>
<button onclick={this.handleClick}>Change Message</button>)
}
}
const mapStateToProps = state => {
// state variables linked in the reducer
}
export default connect(mapStateToProps)(SmartComponent)
let DumbComponent = ({ message }) => {
return ({message})
}
Should you do it ?
If the data you display in this component can be completely isolated from the rest of your application, that is to say no dispatched action could modify it, and no other component need it, keeping this data in the store can be unnecessary.
I mostly use this method to perform optimistic updates to the view component without altering the store until new value is saved by the server.

Prop always null in react

I have a component which has to display the object details. This is instantiated from a TableComponent on click of a row.
The object id is passed:
<ObjectDetails objectID = {id}/>
This is my ObjectDetails component:
class ObjectDetails extends Component {
componentWillMount() {
this.props.dispatch(loadObjectDetails(this.props.objectID));
}
render() {
console.log("props------" + JSON.stringify(this.props));
....
}
let select = (state) => ({objectDetails: state.objectDetails});
export default connect(select)(ObjectDetails);
}
loadObjectDetails populates the store with objectDetails. I can see that the store does have the details.
But in render(), props always has objectDetails as null.
Not sure what I'm doing wrong, any help please?
Edit:
Adding few more details
export function loadObjectDetails(objectID) {
return function (dispatch) {
Rest.get('/rest/objects/' + objectID).end(
(err, res) => {
if(err) {
dispatch({ type: 'FETCH_OBJECT_DETAILS_FAILURE', error: res.body})
} else {
dispatch({ type: 'FETCH_OBJECT_DETAILS_SUCCESS', objectDetails: res.body})
}
}
)
}
}
export default function objectDetailsReducer(state={
objectDetails: null,
error: null,
}, action) {
switch (action.type) {
case "FETCH_OBJECT_DETAILS_SUCCESS": {
return {...state, objectDetails: action.objectDetails, error: null}
}
case "FETCH_OBJECT_DETAILS_FAILURE": {
return {...state, error: action.error }
}
}
return state
}
const middleware = applyMiddleware(promise(), thunk, logger())
export default compose(middleware)(createStore)(combineReducers({ reducer1, reducer2, objectDetailsReducer}))
The object inside your mapStateToProps function is what gets passed as props:
{objectDetails: state.objectDetails}
So in your component, you can access: this.props.objectDetails. And whatever properties are on that. If you just wish to pass the objectID, update your mapStateToProps function to change that.
if you want component get new props when store mutates, you have to use mapStateToProps can see many samples in docs: http://redux.js.org/docs/basics/UsageWithReact.html
In your sample code you have extra spaces around = sign:
<ObjectDetails objectID = {id}/>.
componentWillMount function is fired once when component will mount and that's it, so you dispatch this action, it updates the store, but you don't have mapStateToProps so this component has no reaction to store updates.
Component can react to store by mapStateToProps or you can leave this component presentational but then upper component has to have mapStateToProps.
You can read about presentational component on same page: http://redux.js.org/docs/basics/UsageWithReact.html
Component will try to re-render only when props or state is changed. If props/state is not changed - no re-render happens. Your props don't change. You don't use state on this component at all, so nothing changes too.
The render() method was throwing an error on the first render which is why it was not rendering on props change.
It is re-rendering after I fixed that error.
Sorry for wasting your time!

Resources