Proper Way to merge new props into state - javascript

I am currently working on calendar component using react-big-calendar.
I currently have one Wrapper for it that pulls data from an database and sends it into the Calendar Component. However, I am making a feature that one can 'send' calendar events to that component from anywhere.
Currently I have only have use cases, which is from the website's built in notification system, there will be a feature to add an external event sent over as a notification to your own calendar.
I might however want to push events to users calendars from another webapplication through my socket server (socket.io).
I am not sure what is the "proper" way to go about it in react, technically.
How I went about it now is that I added
static getDerivedStateFromProps(nextProps, prevState){
if(nextProps.propInput === true){
nextProps.reset()
return {events : {...prevState.events, ...nextProps.events}}
} else return null
}
inside my CalendarWrapper, which basically does the following for me:
1) I update the props sent into the wrapper, either from parent component or from the redux store (i will be using the 2nd)
2) This triggers getDrivedStateFromProps and checks if it receives propInput as true, and if that's the case, it merges the new events being sent down as props into the current events of the calendar.
3) it then runs a callback function (nextProps.reset()), to reset propInput to FALSE, which triggers another run of getDrivedStateFromProps, but this time returns null and doesn't cause a new setState.
This was my own dreamed up solution on HOW to push new events to this calendar wrapper from the redux store. It seems unorthodox, but for me it's the only way to have a middle ground with pure redux and everything saved in redux, and having local state of each component.
Is there a technically better and more perfomant way to go about solving this problem?
Thanks in advance.

I would save a timestamp (instead boolean propInput) with new events sent to redux. Comparing 'last update time' is enough to take a decision of update - no need to clear propInput flag (no reset() call ... reducers... etc.).
You don't even need to store this timestamp in state - no need to use getDerivedStateFromProps ("fired on every render, regardless of the cause") - classic solution:
componentDidUpdate(prevProps) {
if (this.props.newEventsTime !== prevProps.newEventsTime) {
setState({events : {...this.state.events, ...this.props.newEvents}})
}
}
More optimal should be
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.newEventsTime !== this.props.newEventsTime) {
setState({events : {...this.state.events, ...this.props.newEvents}});
return false; // no render needed in this pass
}
return (nextState.events === this.state.events) ? false : true;
}
- update on events object ref change.

You have to return a merge of your prevState with your new state, like this:
static getDerivedStateFromProps(nextProps, prevState){
if(nextProps.propInput === true){
nextProps.reset()
return {...prevState, ...{
events : nextProps.events
}
}
} else return null
}

Related

React: Managing form state with a combination of useReducer and useState

The component I'm working on is a time input for a form. The form is relatively complex and is generated dynamically, with different fields appearing based on data nested inside other data. I'm managing the state of the form with useReducer, which has worked very well so far. Now that I'm trying to implement a time input component, I'd like to have some basic validation, in particular so I don't get junk non-formatted data into my database. My way of thinking about it was that my database wants one thing: a time, formatted per ISO8601. The UI on the other hand could get that date any number of ways, in my case via an "hour" field, a "minute" field, and eventually an am/pm field. Since multiple fields are being individually validated, and then combined into a single ISO string, my approach was to have useState manage the individual fields and their validation, and then dispatch a single processed ISO string to my centralized state.
To get that to work I tried having the onChange listener of the input fields simply update the local state with a validated input, and then have useEffect "listen" to the local state using its dependency array. So each time local state changes, the useEffect callback dispatches an action with the new input, now processed into an ISO string, in its payload. I was a bit surprised this worked, but I still have a lot to learn about.. all of it. Anyways this worked great, or so I thought..
Since the component in question, TimePiece, is being rendered dynamically (inside nested loops) inside of its parent's parent component, when the user changes the form a bit, the TimePiece component gets rendered with new props and state. But therein lies the rub, every time TimePiece is rendered, it has the same state as every other "instance" of TimePiece (it's a function component though). I used some console.logs to find out it's actually maintaining it's separate state until the moment in renders, when it's then set to the state of the last "instance" that was modified.
My central useReducer state is keyed by a series of ids, so it's able to persist as the user changes the view without a similar problem. It's only the local state which isn't behaving properly, and somewhere on the re-render it sends that state to the central useReducer state and overwrites the existing, correct value...
Something is definitely off, but I keep trying different version and just breaking the thing. At one point it was actually fluttering endlessly between the two states... I thought I would consult the internet. Am I doing this completely wrong? Is it some slight tweak? Should I not have dispatch inside of useEffect with a local state dependency?
In particular, is it strange to combine useState and useReducer, either broadly or in the specific way I've done it?
Here is the code.. if it makes no sense at all, I could make a mock version, but so often the problem lies in the specifics so I thought I'd see if anyone has any ideas. Thanks a bunch.
The functions validateHours and validateMinutes shouldn't have much effect on the operation if you want to ignore those (or so I think.....).
"Mark" is the name of the field state as it lives in memory, e.g. the ISO string.
io is what I'm calling the user input.
function TimePiece({ mark, phormId, facetParentId, pieceType, dispatch, markType, recordId }) {
const [hourField, setHourField] = useState(parseIsoToFields(mark).hour);
const [minuteField, setMinuteField] = useState(parseIsoToFields(mark).minute);
function parseFieldsToIso(hour, minute) {
const isoTime = DateTime.fromObject({ hour: hour ? hour : '0', minute: minute ? minute : '0' });
return isoTime.toISOTime();
}
function parseIsoToFields(isoTime) {
const time = DateTime.fromISO(isoTime);
const hour = makeTwoDigit(`${time.hour}`);
const minute = makeTwoDigit(`${time.minute}`);
return {
hour: hour ? hour : '',
minute: minute ? minute : ''
}
}
function makeTwoDigit(value) {
const twoDigit = value.length === 2 ? value :
value.length === 1 ? '0' + value : '00'
return twoDigit;
}
function validateHours(io) {
const isANumber = /\d/g;
const is01or2 = /[0-2]/g;
if (isANumber.test(io) || io === '') {
if (io.length < 2) {
setHourField(io)
} else if (io.length === 2) {
if (io[0] === '0') {
setHourField(io);
} else if ( io[0] === '1' && is01or2.test(io[1]) ) {
setHourField(io);
} else {
console.log('Invalid number, too large..');
}
}
} else {
console.log('Invalid characeter..');
}
}
function validateMinutes(io) {
const isANumber = /\d/g;
const is0thru5 = /[0-5]/;
if (isANumber.test(io) || io === '') {
if (io.length < 2) {
setMinuteField(io);
} else if (is0thru5.test(io[0])) {
setMinuteField(io);
} else {
console.log('Invalid number, too large..');
}
} else {
console.log('Invalid character..');
}
}
useEffect(() => {
dispatch({
type: `${markType}/io`,
payload: {
phormId,
facetId: facetParentId,
pieceType,
io: parseFieldsToIso(hourField, minuteField),
recordId
}
})
}, [hourField, minuteField, dispatch, phormId, facetParentId, pieceType, markType, recordId])
return (
<React.Fragment>
<input
maxLength='2'
value={hourField} onChange={(e) => {validateHours(e.target.value)}}
style={{ width: '2ch' }}
></input>
<span>:</span>
<input
maxLength='2'
value={minuteField}
onChange={(e) => { validateMinutes(e.target.value) }}
style={{ width: '2ch' }}
></input>
</React.Fragment>
)
}
P.S. I made another version which avoids using useState and instead relies on one functions to validate and process the fields, but for some reason it seemed weird, even if it was more functional. Also having local state seemed ideal for implementing something that highlights incorrect inputs and says "invalid number" or whatever, instead of simply disallowing that input.
EDIT:
Live code here:
https://codesandbox.io/s/gv-timepiecedemo-gmkmp?file=/src/components/TimePiece.js
TimePiece is a child of Facet, which is a child of Phorm or LogPhorm, which is a child of Recorder or Log... Hopefully it's somewhat legible.
As suggested I managed to get it working on Codesandbox. I was running a local Node server to route to a Mongo database and didn't know how to set that up, so I just plugged it with a dummy database, shouldn't effect the problem at hand.
To create the problem, in the top left dropdown menu, choose "Global Library", and then click on either "Pull-Up" or "Push-Up". Then in the main window, try typing in to the "Time" field. "Pull-Up" and "Push-Up" are both using this TimePiece component, when you click on the other one, you'll see that the Time field there has changed to be the same as other Time field. The other fields ("Reps", "Load") each maintain their own independent state when you switch between exercises, which is what I'm going for.
If you click "generate record" withs some values in the "Time" field, it makes a "record" which will now show up on the right side. If you click on that it expands into a similar display as the main window. The same problem happens over here with the "Time" field, except the state is independent from the state in the main window. So there are basically two states: one for all Time fields in the main window, one for all Time fields in the right window. Those are being rendered by different parents, Phorm and LogPhorm respectively, maybe that is a hint?
Thanks all!!
Ok, after spending a few hours just trying to trace the data flow from TimePiece back through all the abstraction to "state", and back, and all I can really say is that you've a ton of prop drilling. Almost all your components consume the same, or very similar, props
What I finally found is that TimePiece doesn't unmount when switching between what I guess you are calling Phorms(??), which you've abstracted via a Widget. Once I found what wasn't unmounting/remounting as I'd expect to display the different hours & minutes state the solution was simple: Add a React key corresponding to the Phorm when you switch between pull-ups and push-ups.
Phorm.js
<Widget
key={phormId} // <-- add react key here
mark={marks[facetParentId][piece.pieceType]}
phormId={phormId}
facetParentId={facetParentId}
dispatch={dispatch}
pieceType={piece.pieceType}
markType={markType}
recordId={recordId}
/>
Using a react key here forces React to treat the two exercises widget time pieces as two separate "instances", when you switch between the two the component remounts and recomputes the initial component state in TimePiece.

How to show Toast only once after received error message prop from Redux store

I have working React-Native + Redux registration flow:
Fill inputs
Validate them in component (Formik)
Call action to store registerUser(formData)
Wait for saga to do async call to API
On API error call reducer REGISTER_ERROR what sets store variable formError to some message.
I have my component with mapped state to props (this error message is hooked to prop).
I am doing componentDidUpdate() and when the error prop from store is changed I fire ToastAndroid.show(errorMessage).
But my Toast is called multiple times, because componentDidUpdate is also called multiple times (Redux updating component multiple times).
I know quick workaround by for example creating local state visible and when this state variable is true then no other Toasts are shown.
Is there any better more common way to do it? It is pretty weird in my opinion to rely on Toast's onClose event to set the state variable to false.
As stated in the first answer it'd be good to see the actual code, but what you should be doing is only showing the toast message when the prop changes like this (assuming it's a boolean)
componentDidUpdate(prevProps) {
if(prevProps.showToast === false && this.props.showToast === true){
showToast();
}
}
Without the code, it's a bit hard to try coming up with a solution but I think I know at a high-level what you are trying to do.
If I were you, I would make the presentational component (toast UI in this context) just react to the store props/observables instead of calling the ToastAndroid.show() method directly in the life cycle method.
In terms of architecture pattern, I find this pattern works well with react applications. Hope this helps. https://medium.com/#dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

Where to set state, dependent on previous and current props, in React?

I was using the componentWillReceiveProps lifecycle event to enable or disable transition to the next page. Now that this event is changed to UNSAFE_componentWillReceiveProps, I feel like I shouldn't use it anymore, however I couldn't find an obvious replacement for it.
The location of the component comes from props.location.pathname, so I'd need an event, where I can access both the previous and next props and then set the initial appearance of the component depending on if there should be transition or not, however:
getDerivedStateFromProps only has access to the previous props.
shouldComponentUpdate should be used for telling the component if it should update or not, which is not what we want, so it's out.
render doesn't have the previous props.
getSnapshotBeforeUpdate passes the parameter to componentDidUpdate, at which point the component is already rendered, so I can't set the initial apprearance.
I guess I could save the previous pathname and use that the next time in render, but this doesn't seem like an elegant solution. What is the best practice in this situation?
You stated that
getDerivedStateFromProps only has access to the previous props.
But getDerivedStateFromProps has access to next props and previous state
Saving the previous pathname may not seem elegant, I agree, but it is an alternative offered here: https://codesandbox.io/s/rjyvp7l3rq found here https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html.
Take a look at the uncontrolledEmailInput component saving prev props in state
state = {
email: this.props.defaultEmail,
prevPropsUserID: this.props.userID
};
static getDerivedStateFromProps(props, state) {
// Any time the current user changes,
// Reset any parts of state that are tied to that user.
// In this simple example, that's just the email.
if (props.userID !== state.prevPropsUserID) {
return {
prevPropsUserID: props.userID,
email: props.defaultEmail
};
}
return null;
}
Here is a great article about the new lifecycles: https://medium.com/#baphemot/understanding-react-react-16-3-component-life-cycle-23129bc7a705
You can use below which is Safe and recommend by Reactjs
componentDidUpdate(prevProps) {
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
As per react documentation you should use componentDidUpdate. It has access to prevProps, prevState and current props, state in this.props, this.state.
Take a look at the documentation
https://reactjs.org/docs/react-component.html#componentdidupdate
componentDidUpdate(prevProps) {
if(this.props.pathname !== prevProps.pathname) {
//do something here or setState to adjust the appearance accordingly
}
}

Callbacks using redux-thunk / redux-observable with redux

I am learning how redux works but its a lot of code to do simple things. For example, I want to load some data from the server before displaying. For editing reasons, I can't simply just use incoming props but I have to copy props data into the local state.
As far as I've learned, I have to send a Fetch_request action. If successful, a fetch_success action will update the store with new item. Then updated item will cause my component's render function to update.
In component
componentWillMount() {
this.props.FETCH_REQUEST(this.props.match.params.id);
}
...
In actions
export function FETCH_REQUEST(id) {
api.get(...)
.then(d => FETCH_SUCCESS(d))
.catch(e => FETCH_FAILURE(e));
}
...
In reducer
export function FETCH_REDUCER(state = {}, action ={}) {
switch (action.type) {
case 'FETCH_SUCCESS':
return { ...state, [action.payload.id]: ...action.payload }
...
}
Back in component
this.props.FETCH_REDUCER
// extra code for state, getting desired item from...
Instead, can I call a react-thunk function and pass some callback functions? The react-thunk can update the store and callbacks can change the component's local state.
In component
componentWillMount() {
this.props.FETCH_REQUEST(this.props.match.params.id, this.cbSuccess, this.cbFailure);
}
cbSuccess(data) {
// do something
}
cbFailure(error) {
// do something
}
...
In action
export function FETCH_REQUEST(id, cbSuccess, cbFailure) {
api.get(...)
.then(d => {
cbSuccess(d);
FETCH_SUCCESS(d);
}).catch(e => {
cbFailure(d);
FETCH_FAILURE(e);
});
}
...
Is this improper? Can I do the same thing with redux-observable?
UPDATE 1
I moved nearly everything to the redux store, even for edits (ie replaced this.setState with this.props.setState). It eases state management. However, every time any input's onChange fires, a new state is popping up. Can someone confirm whether this is okay? I'm worried about the app's memory management due to redux saving a ref to each state.
First of all, you should call your API in componentDidMount instead of componentWillMount. More on this at : what is right way to do API call in react js?
When you use a redux store, your components subscribe to state changes using the mapStateToProps function and they change state using the actions added a props through the mapDispatchToProps function (assuming you are using these functions in your connect call).
So you already are subscribing to state changes using your props. Using a callback would be similar to having the callback tell you of a change which your component already knows about because of a change in its props. And the change in props would trigger a re-render of the component to show the new state.
UPDATE:
The case you refer to, of an input field firing an onChange event at the change of every character, can cause a lot of updates to the store. As mentioned in my comments, you can use an api like _.debounce to throttle the updates to the store to reduce the number of state changes in such cases. More on handling this at Perform debounce in React.js.
The issue of memory management is a real issue in real world applications when using Redux. The way to reduce the effect of repeated updates to the state is to
Normalize the shape of state : http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html
Create memoized selectors using Reselect (https://github.com/reactjs/reselect)
Follow the advice provided in the articles regarding performance in Redux github pages (https://github.com/reactjs/redux/blob/master/docs/faq/Performance.md)
Also remember that although the whole state should be copied to prevent mutating, only the slice of state that changes needs to be updated. For example, if your state holds 10 objects and only one of them changes, you need to update the reference of the new object in the state, but the remaining 9 unchanged objects still point to the old references and the total number of objects in your memory is 11 and not 20 (excluding the encompassing state object.)

Call componentDidMount when API responds

In my project I have a call to an action that makes a webservice call and in turn dispatch actions to the result of the ws, these actions edit the store.
My problem is in :
ComponentDidUpdate () {
If (this.props.messages.length) {
Const items = this.props.messages.filter (this.isDisplayable);
This.timer = setInterval (() => {
If (items.length> 0) {
This.props.popItem (items);
} Else {
ClearInterval (this.timer);
}
}, This.props.interval);
}
}
In fact it is launched several times and I have warnings of
Warning: flattenChildren (...): Encountered two children with the same
key, 1. Child keys must be unique; When two children share a key,
only the first child will be used.
I used the componentDidMount but it launches it before api responds.
my question is:
Is that there is a way to update the component only at the response of my action, or alternatively to pass the warnings ?
try this :
componentWillReceiveProps(nextProps) {
if (this.props.messages === nextProps.messages) return;
i had some probleme and i resolve it by force update
forceUpdate () {
If (this.props.messages.length) {
...
}
}
In my project I have a call to an action that makes a webservice call and in turn dispatch actions to the result of the ws, these actions edit the store.
None of the methods componentDidMount and componentDidUpdate are good.
Observe the Store in Redux and update your component accordingly when the correct action TYPE is found.
Since you are using the Redux architecture, the state for all your components is in a single place — in the Store.
yes i know, but the problem is that componentDidUpdate is called several times which gives me the index error.
This is quite normal in React. Check this lifecycle.
What you should do is the govern the Redux architecture.
I will try today to provide some diagrams for you.
In general, anything you do will be from the global Store.
You may forget the React.Component state, and props you had in the non-Redux applications.
You typically need to use the Wrapper as a context provider around your app, where the context is the property of React.Component.
The context will be passed to all children and grandchildren so this will be the global Store organization.
Then you will need to read the Store from the context, and call the two typical methods: dispatch and subscribe.

Categories

Resources