Sync local react state with data from redux - javascript

I have an App component which contains all the routes
App.jsx
import Editor from './Editor';
function App(props) {
return (
<BrowserRouter>
<Switch>
<Route path="/cms/post/edit/:postId" component={Editor} />
</Switch>
</BrowserRouter>
);
}
export default App;
I have an Editor component where user can edit a post. This component maps the data from redux store to local state as data needs to be manipulated locally.
Editor.jsx
// Usual Imports
class Editor extends Component {
constructor(props) {
super(props);
this.state = {
title: props.post ? props.post.title : '',
content: props.post ? props.post.content : ''
};
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
this.props.post ? null : this.fetchPosts(this.props.match.params.postId);
}
handleChange(e) {
this.setState({
[e.target.name]: e.target.value
});
}
updatePost(data) {
// function to updatePost
}
fetchPosts(id) {
// function to fetch Posts
}
render() {
return (
<React.Fragment>
<input type="text" name="title" value={this.state.title} onChange={this.handleChange} />
<input type="text" name="content" value={this.state.content} onChange={this.handleChange} />
</React.Fragment>
);
}
}
const mapStateToProps = (state, ownProps) => ({
post: state.posts[ownProps.match.params.postId] || false,
...ownProps
}),
mapDispatchToProps = dispatch => ({
updatePost: data => dispatch(updatePost(data)),
fetchPosts: params => dispatch(fetchPosts(params))
});
export default connect(mapStateToProps, mapDispatchToProps)(Editor);
Now my questions are
Initially, post data is available and fetchPosts is not called. However, if user refreshes the page then post becomes false and fetchPosts is called and redux store is updated.
In which lifecycle method should I update the local react state with data from props?
Possible solutions which I think could be
A. Updating state in componentWillReceiveProps
componentWillReceiveProps(nextProps) {
this.setState({
title: nextProps.post.title,
content: nextProps.post.content
});
}
However, React docs discourages using componentWillReceiveProps as it might be invoked many times in React 16 and so on.
B. Update state in componentDidUpdate
componentDidUpdate(prevProps, prevState) {
if (this.props.post != prevProps.post) {
this.setState({
title: this.props.title,
content: this.props.content
});
}
}
I am not sure about this as I think there might be hidden side effects of this approach.
C. Not setting initial state and providing value to input tags via props i.e. value={this.props.post.title} and updating state via onChange handlers. However, when value is undefined then React will throw an error.
D. Newer lifecycle methods like getDerivedStateFromProps. Not too sure as React docs says it should be used in rare cases when state changes based on props over time.
I need to maintain the state as current component is also used for creating a fresh post also.
Which approach would be the best? And if I am missing out on something then let me know! Thanks!

Related

Port redux state to component state when the component mounts (without need for GET request)

I have a form in a React/Redux application that is used to update information - hence require the fields to be pre-populated with current data.
Before the component is mounted, the data for the form is already sitting in Redux state.
Currently, within the componentDidMount()lifecycle, an axios GET request is sent to retrieve the information from the database again and loads it into the redux state.
This method works fine, however I would like to avoid the additional/redundant GET request as the information is already in the redux state.
How do I port the redux state to the component's state when it loads, so that the input fields are populated (without the need for the GET request)?
component code is below.
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Navbar from '../components/layout/Navbar';
import Sidebar from '../components/layout/Sidebar';
import Breadcrumbs from '../components/layout/Breadcrumbs';
import TextFieldGroup from '../components/form-components/TextFieldGroup';
import { getPatientById } from '../redux/actions/patient.actions';
class PatientEdit extends Component {
constructor() {
super();
this.state = {
firstName: '',
lastName: '',
errors: {}
};
}
componentDidMount = () => {
if (this.props.match.params.patient_id) {
this.props.getPatientById(this.props.match.params.patient_id);
}
};
componentWillReceiveProps = nextProps => {
if (nextProps.errors) {
this.setState({ errors: nextProps.errors });
}
if (nextProps.patients.patient) {
const patient = nextProps.patients.patient;
this.setState({
firstName: patient.firstName.patients,
lastName: patient.lastName.patients
});
}
};
onChange = e => {
this.setState({ [e.target.name]: e.target.value });
};
onSubmit = (e, patient_id) => {
e.preventDefault();
// boring script to handle form submission...
};
render() {
const { errors } = this.state;
const { patient } = this.props.patients;
return (
<>
<Navbar />
<div className="app-body">
<Sidebar />
<main className="main">
<div className="container">
<form onSubmit={e => this.onSubmit(e, patient._id)}>
<div>
<input
type="text"
name="firstName"
value={this.state.firstName}
onChange={this.onChange}
/>
<input
type="text"
name="lastName"
value={this.state.lastName}
onChange={this.onChange}
/>
</div>
<div>
<Link to="/patients" className="btn btn-light mr-2">
Cancel
</Link>
<button type="submit" className="btn btn-primary">
Submit
</button>
</div>
</form>
</div>
</main>
</div>
</>
);
}
}
PatientEdit.propTypes = {
getPatientById: PropTypes.func.isRequired,
patients: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
patients: state.patients,
errors: state.errors
});
export default connect(
mapStateToProps,
{ getPatientById }
)(PatientEdit);
getPatientById action
export const getPatientById = id => dispatch => {
dispatch(setPatientLoading());
axios
.get(`/api/patients/${id}`)
.then(res => {
dispatch({
type: GET_PATIENTS_SINGLE,
payload: res.data
});
})
.catch(err => {
dispatch({
type: GET_ERRORS,
payload: err.response.data
});
});
};
It seems you are copying the data from redux to local state. That might be needed, or not. As for your goal, why not directly render the data received from Redux (without copying them to state)? In that case, you can skip the axios call in componentDidMount.
If you want to have data from Redux in state anyway, you could copy them to state in constructor or in componentDidMount. This makes the copy only once though. If you then need to keep data from redux and state in sync, you need to ensure this in componentWillReceiveProps.
I believe the problem you encountered with your current set up is that componentWillReceiveProps isn't called for first render, hence nothing was copied to your state.
On a remount, couldn't you pass down Redux state and use that for control logic? I am assuming it comes from patients Redux state as you're using it in the componentWillReceiveProps lifecycle method, so if that is the case, couldn't you do:
// rest of code emitted for brevity
componentDidMount = () => {
// assuming init state for patients.patient is null
if (!this.patients.patient && this.props.match.params.patient_id) {
this.props.getPatientById(this.props.match.params.patient_id);
}
};
This assumes this.props.patients.patient initial state is null from the reducer. If this.props.patients is null instead, then change the control logic code to be !this.props.patients && this.props.match.params.patient_id.
The control logic here is saying "If patients is null and we have the param ID then make the API GET call". If you already have the patient, then it won't bother.

Wait for data to be fetched in child components, then render

I have a React app that uses multiple fetch calls throughout different components. In Home page component, I have imported smaller components, all of whom have it's own fetch call.
render() {
return (
<React.Fragment>
<Banner/>
<Services />
<About />
</React.Fragment>
)
}
Banner, Services and About have their own fetch calls to different endpoints, now my question is because the response is a little bit on the slower side, how to wait for all of the child components to fetch data, then render the Homepage component. I have tried to put the state of isLoading and add a loader to wait for components to fetch, but I don't know what to wait for to set isLoading to false.
...how to wait for all of the child components to fetch data, then render the Homepage component
You don't. Instead, you move the fetches to the Homepage component's parent, and then have that parent only render the Homepage component when it has all of the necessary information to do so. In React parlance, this is "lifting state up" (e.g., up the hierarchy to the parent).
While you could render the Homepage in a "loading" form, and have it render its child components in a "loading" form, and have the child components call back to the Home page to say they have their information now, that's more complicated than simply lifting the state up to the highest component that actually needs it (so it knows it can render the Homepage).
As #TJCrowder mentioned in his answer, You'll need to lift your state up and keep it in the parent component. Make a network request there and pass the data to your child component as props. You can read more about lifting-state-up here
class YourComponent extends React.Component {
state = {isLoading: true, isError: false, banner: null, services: null, about: null};
async componentDidMount() {
try {
const [banner, services, about] = await Promise.all([
// all calls
]);
this.setState({ isLoading: false, banner, services, about });
} catch (error) {
this.setState({ isError: true, isLoading: false });
}
}
render() {
if (this.state.isLoading) {
return <div>Loading...</div>
}
return (
<React.Fragment>
<Banner data={this.state.banner} />
<Services data={this.state.services} />
<About data={this.state.about} />
</React.Fragment>
)
}
}
using promises in fetch you could, as suggested, have a isLoaded property state determine whether or not a component should render or not.
class ShouldRender extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
isLoaded: false,
}
}
componentDidMount() {
fetch('http://someresource.com/api/resource')
.then(res => res.json())
.then(data => {
this.state({
data,
isLoaded: true,
});
})
}
render() {
const { isLoaded } = this.state;
if (isLoaded) {
return <MyAwesomeReactComponent />
}
return null;
}
}
So once the state is updated it will trigger a rerendering of the component with the new state that will render the if statement true and you're JSX will appear.

View doesn't want to re-render after store dispatch is called in Redux (React)

I need to change text input value after the state in redux has been changed (re-render the view). That means everything from text input will be stored in redux store (after each character) and re-rendered back to the input. If I use setState without redux, the view is changed successfully but also if I use this.forceUpdate() method for force re-render in handleChange function.
I think that's problem of the fact, that state in reducer isn't changed properly. I googled many way to do it but nothing doesn't worked for me.
Reduced code here:
'use strict';
import * as React from "react";
import { createStore } from 'redux';
interface State
{
}
interface Props
{
}
function cardReducer(state = {redirect : false, cardId : ""}, action) {
console.log("reducer action: " + JSON.stringify(action));
switch (action.type) {
case 'TYPING':
return Object.assign({}, state, {
redirect : false,
cardId : action.cardId
});
default:
return state
}
}
const store = createStore(cardReducer);
console.log("STORE state: " + JSON.stringify(store.getState()));
export class Home extends React.Component<Props, State> {
constructor(props : any)
{
super(props);
}
handleChange = (e) => {
store.dispatch({ type : 'TYPING', cardId : store.getState().cardId + e.target.value});
};
render()
{
return (
<div className={"container cardwrapper offset-md-2 col-md-8"}>
<form>
<input value={store.getState().cardId} onChange={this.handleChange} id={"card_id"} className={"command"} type="text" autoFocus autoComplete={"off"}/>
</form>
);
}
}
}
You're missing connect and mapStateToProps (Google these two). Doing store.getState() in the component is not how React listens for updates.
After getting the data from store.getState(), you need to set it on your state as well for your component to be re rendered.
You can do it as follows:
Put cardId on your state inside the constructor as follows:
this.state {
cardId: ""
}
Then on getting update from store, set the state as:
this.setState({
cardId: store.getState().cardId;
});

React native: passing in multiple parent props to child component

I have a react native application and I am trying to pass in props to my AppwithNavigationComponent. I want to do this so that the props created in Authenticator component are passed into the AppwithNavigationState component. However I am stuck on passing in multiple variables to a component:
---- index.js-----
render() {
return (
<ApolloProvider store={store} client={client}>
<Authenticator hideDefault={true} onStateChange={this.handleAuthStateChange}
theme={Object.assign(AmplifyTheme, styles)}>
<AppWithNavigationState { ...this.props }/> // passing in props
</Authenticator>
</ApolloProvider>
)
}
}
export default App = codePush(App);
--- AppNavigator.js ------
const AppWithNavigationState = (props,{ dispatch, nav }) => { // trying to pass in props from parent component, get following error when I do so
console.log(props);
return (
<AppNavigator navigation={addNavigationHelpers({ dispatch, state: nav })} screenProps={{ ...props }} />
)};
AppWithNavigationState.propTypes = {
dispatch: PropTypes.func.isRequired,
nav: PropTypes.object.isRequired,
};
const mapStateToProps = state => ({
nav: state.nav,
});
export default connect(mapStateToProps)(AppWithNavigationState);
^^ When I try to pass in parent props into this component, its as if the dispatch and nav components are ignored. Am I not passing in props the way I am supposed to? is there a way to pass in multiple props? if I pass in the parent props I then get the following error message:
Unhandled JS Exception: TypeError: Cannot read property 'index' of undefined
This error is located at:
in Transitioner (at CardStackTransitioner.js:60)
in CardStackTransitioner (at StackNavigator.js:48)
in Unknown (at createNavigator.js:36)
in Navigator (at createNavigationContainer.js:198)
However if I don't try to add in the parent props then everything works fine.
I think you just need to change:
const AppWithNavigationState = (props,{ dispatch, nav }) =>
to:
const AppWithNavigationState = ({ dispatch, nav }) =>
Functional components by default have just a single parameter, the props object. If the passed in props have the dispatch and nav on the props object, the above destructuring syntax just gives you the dispatch and nav variables straight away.
See docs for examples of functional components.

React Meteor, reactive prop and inner state

I have a component
import React, { Component } from 'react'
import { EditorState, convertToRaw } from 'draft-js'
import { Editor } from 'react-draft-wysiwyg'
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'
import draftToHtml from 'draftjs-to-html'
import toolbarOptions from './JpuriTextEditorOptions'
export default class TextEditor extends Component {
state = {
editorState: this.props.editorState
}
componentWillReceiveProps = nextProps => {
console.warn('componentWillReceiveProps')
console.log('nextProps.editorState', nextProps.editorState)
console.log('this.props.editorState', this.props.editorState)
this.setState({editorState: nextProps.editorState})
}
onEditorStateChange = editorState => this.setState({editorState})
onBlur = () => this.props.onBlur({key: this.props.myKey, value: draftToHtml(convertToRaw(this.state.editorState.getCurrentContent()))})
render () {
return (
<Editor
wrapperClassName={this.props.wrapperClassName}
editorClassName={this.props.editorClassName}
toolbarClassName={`toolbarAbsoluteTop ${this.props.toolbarClassName}`}
wrapperStyle={this.props.wrapperStyle}
editorStyle={this.props.editorStyle}
toolbarStyle={this.props.toolbarStyle}
editorState={this.state.editorState}
onEditorStateChange={this.onEditorStateChange}
onChange={this.props.onChange}
toolbarOnFocus
toolbar={toolbarOptions}
onFocus={this.props.onFocus}
onBlur={this.onBlur}
onTab={this.props.onTab}
/>
)
}
}
I pass to it a reactive prop, this.props.editorState
Then I set it inside the internal state to handle changes there. And only onBlur I save my changes to the mongo db.
Now there is a problem.
Whenever I click on the editor I see componentWillReceiveProps logs a few times. And it happens on every change thus I am not able to use my editor component properly. As it's cursor is being reset to the first letter with every click and change.
I am using this library of draftjs https://github.com/jpuri/react-draft-wysiwyg
EDIT
More specifics for the question.
setting the state to this.props.editorState in the constructor or in the componentDidMount is solving the issue for the initial state.
But there is still just one problem left.
In another component I have undo redo functionality that works directly with the db. Now if I type some text. Blur it my change is saved to the db and I can see the text because of the internal text. However if I click the undo button, the text will be undone from the db, however it will still be visible in the editor because of the internal state. So I will have to refresh to see my action undone.
With componentWillReceiveProps this issue is solved, however for some reason componentWillReceiveProps is being called every time the state changes even though the props are not changed. Thus bringing above mentioned issues.
If you are using the Controlled pattern of this component then you should set the state internally and not set it from the outside via props.
For example, if you set the state on each new prop received then theres no reason to use an internal state.
According to their DOCS it seems like you are following their example of controlled EditorState except you are overriding your state on each new prop.
I think if you will just remove this behavior from componentWillReceiveProps
i.e setting the state: this.setState({editorState: nextProps.editorState})
It should work like expected.
This is their example by the way:
import React, { Component } from 'react';
import { EditorState } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
class ControlledEditor extends Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
};
}
onEditorStateChange: Function = (editorState) => {
this.setState({
editorState,
});
};
render() {
const { editorState } = this.state;
return (
<Editor
editorState={editorState}
wrapperClassName="demo-wrapper"
editorClassName="demo-editor"
onEditorStateChange={this.onEditorStateChange}
/>
)
}
}
EDIT
A followup to your comment, state initialization is usually made in the constructor.
But when you want to initialize state asynchronously you can't and should not do it in the constructor (nor in componentWillMount) because by the time the async operation will finish the render method will already be invoked.
The best place to do that is componentDidMount or eventHandlers, so in your case the onBlur eventHandler:
import React, { Component } from 'react';
import { EditorState } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
class ControlledEditor extends Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
};
}
onEditorStateChange = (editorState) => {
this.setState({
editorState,
});
};
onBlur = (event) => {
// do async stuff here and update state
};
render() {
const { editorState } = this.state;
return (
<Editor
editorState={editorState}
wrapperClassName="demo-wrapper"
editorClassName="demo-editor"
onEditorStateChange={this.onEditorStateChange}
onBlur={this.onBlur}
/>
)
}
}

Categories

Resources