My app has a user control panel and when the page is loaded it fetch data from the server using Redux.
In construction the component create an initial state like:
const { profile } = this.props;
this.state = {
prop1: profile.prop1 || '',
prop2: profile.prop2 || '',
nested: {
nestedProp1: profile.nested.nestedProp1 || '',
}
...etc...
}
On componentWillMount I have this:
componentWillMount() {
const { user, getProfile } = this.props;
if (user.profile_id) {
getProfile(user.profile_id);
}
}
What I don't understand are 2 things:
Is the approach correct? I'm using state to handle form inputs.
How can I update the state when fetched? There are plenty of properties in this profile object and I was wondering to update all the states in a very simple way, and not one by one...
1.If you are using redux,I think there is no need to use state to manage date, instead you can use props(redux) to handle all the date in your project.
Then,if you want to update the date, you should create action to update the globally unique date that stored in redux.
2.About how to handle the input, when the user have input value, you can create an action, create a copy with the initial state then update state with your input action.
function updateInput(state = initialState, action) {
switch (action.type) {
case 'INPUT':
return Object.assign({}, state, {
profile_id: action.profile
})
return state;
}
}
You could use static getDerivedStateFromProps(props, state) -method to update your component state from props whenever your Redux store updates. It has two params props and state.
class App extends React.Component {
// https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
static getDerivedStateFromProps(props, state){
// Compare and update state only if props differs from state
if(JSON.stringify(state) !== JSON.stringify(props.profile)){
return { ...props.profile }
}
// If it doesn't differ do not update state
return null
}
// Do an state initialization
state = { ...this.props.profile }
// Prefer componentDidMount -for doing fetch and updating component state
componentDidMount(){
const { user, getProfile } = this.props;
if (user.profile_id) {
getProfile(user.profile_id);
}
}
render(){
return (
<div className="App">
{/** Render content */}
</div>
);
}
}
Rest spread operator, what is used to fill up state is ES6 syntax.
If you use Babel you might need to add rest spread operator -plugin to your .babelrc -config. https://babeljs.io/docs/en/babel-plugin-proposal-object-rest-spread
Related
I know, there are many, many similary questions.. **duplicate alarm!**
But: I looked through all of them, I promise. I'm quite sure now, that this is another case, that could have to do with the props being an object (from what I've read here). But I couldn't solve the following, anyway:
class CsvListDropdown extends Component {
constructor(props) {
super(props);
this.state = { sessions: props.sessions }
this csvsInSession = this.csvsInSession.bind(this);
}
csvsInSession(sessions) {
return (sessions
.map(keys => Object.entries(keys)[2][1])
.map((csv, i) => (
<option value={csv} key={i}>{csv}</option>
))
)
}
render() {
const { isLoading } = this.props
if (isLoading) { blablabla.. }
else {
return (
...
<select value={this.props.sessions[0].currentCsv}>
{this.csvsInSession(this.state.sessions)}
</select>
...
)
}
}
}
export default withTracker(() => {
const handle = Meteor.subscribe('sessions');
return {
isLoading: !handle.ready(),
sessions: Sessions.find({}).fetch()
};
})(CsvListDropdown);
Now from the client I am writing another document into the Sessions collection, containing the .csv filename, while this new csv file is being uploaded to a remote server. console.log(this.props.sessions) gives me an array, which is up to date. But the component itself does not re-render.
What I also don't understand is: console.log(this.state.sessions) returns undefined. (note: state)
What I tried so far:
{this.csvsInSession(this.props.sessions)} (note: props)
Adding a withTracker / State / Props to the parent component and passing the sessions object from either state or props as params to the child component, that should re-render.
forceUpdate()
componentWillUpdate()
What may be important as well: The component should re-render about the same time another component also re-renders (which displays the contents of uploaded CSVs, that return from a microservice and get written into another collection). The latter does actually re-render.. But that dropdown does not.. argh!
this.state will only change if you call this.setState(), which you are not doing. You are initializing state with a value from props, but only in the constructor when the component is first instantiated. After that, even if props changes your component may re-render but what it displays won't change because state hasn't been updated.
In fact, there does not appear to be any reason whatsoever to store data in state in that component. It might as well be a functional presentational component:
function CsvListDropdown(props) {
function csvsInSession(sessions) {
return (sessions
.map(keys => Object.entries(keys)[2][1])
.map((csv, i) => (
<option value={csv} key={i}>{csv}</option>
))
)
}
const { isLoading } = props;
if (isLoading) { blablabla.. }
else {
return (
...
<select>
{csvsInSession(props.sessions)}
<select>
...
)
}
}
Generally all of your components should be stateless functional components unless they specifically need to store internal state for some reason.
Now I finally solved it, and it turns out that the component did actually update at any time, but I did not notice it, simply because the latest item in the array was quietly appended to the bottom of the dropdown list. This however I was not expecting, as I had published the collection with a descending sorting.
// server-side
Meteor.publish('sessions', function() {
return Sessions.find({ userId: this.userId }, { sort: {createdAt: -1} });
});
Server-side seems to be the wrong place to sort. It simply does not have an effect. So sorted on the client side, when subscribing:
// client-side
export default withTracker(() => {
const handle = Meteor.subscribe('sessions');
return {
isLoading: !handle.ready(),
sessions: Sessions.find({}, { sort: {createdAt: -1} }).fetch()
};
})(App)
I had omitted an important detail from my question, that is how I set the value of the dropdown field:
<select value={this.props.sessions[0].currentCsv}>
{this.csvsInSession(sessions)}
</select>
So lesson learned: If you think your react component does not re-render, always check if that's true, before assuming so.
As a side effect of debugging I restructered my components. Now the Meteor.subscribe() is within the parent component, that contains all the children, that have to handle the sessions object. And the sessions object gets passed down from the parent to the (grand)children as props. I think it's more readable and easier to maintain that way.
I have a complete running code, but it have a flaw. It is calling setState() from inside a render().
So, react throws the anti-pattern warning.
Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount
My logic is like this. In index.js parent component, i have code as below. The constructor() calls the graphs() with initial value, to display a graph. The user also have a form to specify the new value and submit the form. It runs the graphs() again with the new value and re-renders the graph.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
checkData: true,
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost !== nextProps.cost) {
this.setState({
checkData: true
});
}
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
if (this.props.cost.length && this.state.checkData) {
const tmp = this.props.cost;
//some calculations
....
....
this.setState({
theData: tmp,
checkData: false
});
}
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
<PieGraph theData={this.state.theData} />
</div>
);
}
}
The FormComponent is an ordinary form with input field and a submit button like below. It sends the callback function to the Parent component, which triggers the graphs() and also componentWillReceiveProps.
handleFormSubmit = (e) => {
this.props.onGpChange(this.state.value);
e.preventdefaults();
}
The code is all working fine. Is there a better way to do it ? Without doing setState in render() ?
Never do setState in render. The reason you are not supposed to do that because for every setState your component will re render so doing setState in render will lead to infinite loop, which is not recommended.
checkData boolean variable is not needed. You can directly compare previous cost and current cost in componentWillReceiveProps, if they are not equal then assign cost to theData using setState. Refer below updated solution.
Also start using shouldComponentUpdate menthod in all statefull components to avoid unnecessary re-renderings. This is one best pratice and recommended method in every statefull component.
import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';
const initialval = '8998998998';
class Dist extends Component {
constructor() {
this.state = {
theData: ''
};
this.graphs(initialval);
}
componentWillReceiveProps(nextProps) {
if (this.props.cost != nextProps.cost) {
this.setState({
theData: this.props.cost
});
}
}
shouldComponentUpdate(nextProps, nextState){
if(nextProps.cost !== this.props.cost){
return true;
}
return false;
}
graphs(val) {
//Calls a redux action creator and goes through the redux process
this.props.init(val);
}
render() {
return (
<div>
<FormComponent onGpChange={recData => this.graphs(recData)} />
{this.state.theData !== "" && <PieGraph theData={this.state.theData} />}
</div>
);
}
}
PS:- The above solution is for version React v15.
You should not use componentWillReceiveProps because in most recent versions it's UNSAFE and it won't work well with async rendering coming for React.
There are other ways!
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps is invoked right before calling the render
method, both on the initial mount and on subsequent updates. It should
return an object to update the state, or null to update nothing.
So in your case
...component code
static getDerivedStateFromProps(props,state) {
if (this.props.cost == nextProps.cost) {
// null means no update to state
return null;
}
// return object to update the state
return { theData: this.props.cost };
}
... rest of code
You can also use memoization but in your case it's up to you to decide.
The link has one example where you can achieve the same result with memoization and getDerivedStateFromProps
For example updating a list (searching) after a prop changed
You could go from this:
static getDerivedStateFromProps(props, state) {
// Re-run the filter whenever the list array or filter text change.
// Note we need to store prevPropsList and prevFilterText to detect changes.
if (
props.list !== state.prevPropsList ||
state.prevFilterText !== state.filterText
) {
return {
prevPropsList: props.list,
prevFilterText: state.filterText,
filteredList: props.list.filter(item => item.text.includes(state.filterText))
};
}
return null;
}
to this:
import memoize from "memoize-one";
class Example extends Component {
// State only needs to hold the current filter text value:
state = { filterText: "" };
// Re-run the filter whenever the list array or filter text changes:
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// Calculate the latest filtered list. If these arguments haven't changed
// since the last render, `memoize-one` will reuse the last return value.
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}
I searched a bit about this question but found very vague answers. In redux, we know that the state is stored as an object. But where is this state stored actually? Is it somehow saved as a file which can be accessed by us later on? What I know is that it does not store it in a cookie format or in the browser's local storage.
The state in Redux is stored in memory, in the Redux store.
This means that, if you refresh the page, that state gets wiped out.
You can imagine that store looking something like this:
function createStore(reducer, initialState) {
let state = initialState // <-- state is just stored in a variable that lives in memory
function getState() {
return state
}
function dispatch(action) {
state = reducer(state, action) // <-- state gets updated using the returned value from the reducer
return action
}
return {
getState,
dispatch
}
}
The state in redux is just a variable that persists in memory because it is referenced (via closure) by all redux functions.
Here's a simplified example of what is going on:
function example() {
let variableAvailableViaClosure = 0
function incrementTheClosureVariable() {
variableAvailableViaClosure += 1
}
function getTheClosureVariable() {
return variableAvailableViaClosure
}
return {
incrementTheClosureVariable,
getTheClosureVariable
}
}
let data = example()
// at this point example is finished
// but the functions it returned
// still have access to the (internal) variable via closure
console.log(
data.getTheClosureVariable() // 0
)
data.incrementTheClosureVariable()
console.log(
data.getTheClosureVariable() // 1
)
Furthermore, the statement
In redux, we know that the state is stored as an object.
isn't correct. State in redux can be any valid javascript value, not just an object. It just usually makes the most sense for it to be an object (or a special object like an array) because that allows for a more flexible data structure (but you could make the state just be a number for example, if you wanted to).
Check out the actual Redux implementation for more details.
If you want the state to persist in a cookie or localStorage, you would enhance the store such that, on top of updating the state in memory, it will save to your desired storage as well (and load from that storage when the store is initialized)
States are stored in redux-store. Redux Store is a global store which can be accessed anywhere/any components.
Let consider an example of getting Index of data using third party API. The following snippet uses componentWillMount which will trigger a fetch call using redux action.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchDataFromUrl } from '../actions/index.js';
class Indexdata extends Component {
constructor(props){
super(props);
this.state = {
text: ''
}
}
componentWillMount(){
let thisVal = this;
thisVal.props.fetchIndexofData()
}
componentWillReceiveProps(nextProps){
this.setstate({
text: nextProps.indexData.text
})
}
render(){
return(
<div>
<Navbar />
<h2 className="prescription-index-title">Index of Data</h2>
</div>
)
}
}
function mapStateToProps(state){
return{
indexData: state.fetchedData
}
}
function mapDisptachToProps(dispatch){
return {
fetchIndexofData: () => dispatch(fetchDataFromUrl(access_token))
};
};
export default connect(mapStateToProps, mapDisptachToProps)(IndexData);
The above snippet will fetch index of data using a redux action. The below code is a redux action,
export function fetchDataFromUrl(){
return(dispatch) => {
const base_url = "https://api_serving_url.com"
fetch(base_url, {
method: 'GET'
})
.then(response => response.json())
.then(data => {
dispatch({
type: "INDEX_DATA",
data: data
})
})
}
}
Redux action will dispatch data to reducer, where state will be initialized in redux store. The following code snippet is redux-reducer
export function fetchedData(state = [], action) {
switch(action.type) {
case "INDEX_DATA":
return action.data;
default:
return state;
}
}
State stored in redux store will be mapped using function mapStateToProps, implemented in the above component. Now you can access the state using props in the respective component. Lifecyclehook componentWillReceiveProps will be able to fetch the state stored redux store.
You can access the State by means of using store.getState() in any component.The only drawback of using reducer state, is that it will reset the state when you refresh the component/application. Go through Reducer Store , for more information.
I am unable to get props inside constructor that I have implemented using redux concept.
Code for container component
class UpdateItem extends Component{
constructor(props) {
super(props);
console.log(this.props.item.itemTitle) // output: undefined
this.state = {
itemTitle: this.props.item.itemTitle,
errors: {}
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
//If the input fields were directly within this
//this component, we could use this.refs.[FIELD].value
//Instead, we want to save the data for when the form is submitted
let state = {};
state[e.target.name] = e.target.value.trim();
this.setState(state);
}
handleSubmit(e) {
//we don't want the form to submit, so we pritem the default behavior
e.preventDefault();
let errors = {};
errors = this._validate();
if(Object.keys(errors).length != 0) {
this.setState({
errors: errors
});
return;
}
let itemData = new FormData();
itemData.append('itemTitle',this.state.itemTitle)
this.props.onSubmit(itemData);
}
componentDidMount(){
this.props.getItemByID();
}
componentWillReceiveProps(nextProps){
if (this.props.item.itemID != nextProps.item.itemID){
//Necessary to populate form when existing item is loaded directly.
this.props.getItemByID();
}
}
render(){
let {item} = this.props;
return(
<UpdateItemForm
itemTitle={this.state.itemTitle}
errors={this.state.errors}
/>
);
}
}
UpdateItem.propTypes = {
item: PropTypes.array.isRequired
};
function mapStateToProps(state, ownProps){
let item = {
itemTitle: ''
};
return {
item: state.itemReducer
};
}
function mapDispatchToProps (dispatch, ownProps) {
return {
getItemByID:()=>dispatch(loadItemByID(ownProps.params.id)),
onSubmit: (values) => dispatch(updateItem(values))
}
}
export default connect(mapStateToProps,mapDispatchToProps)(UpdateItem);
Inside render() method am able to get the props i.e. item from the redux but not inside constructor.
And code for the actions to see if the redux implementation correct or not,
export function loadItemByID(ID){
return function(dispatch){
return itemAPI.getItemByID(ID).then(item => {
dispatch(loadItemByIDSuccess(item));
}).catch(error => {
throw(error);
});
};
}
export function loadItemByIDSuccess(item){
return {type: types.LOAD_ITEM_BY_ID_SUCCESS, item}
}
Finally my reducer looks as follows,
export default function itemReducer(state = initialState.item, action) {
switch (action.type) {
case types.LOAD_ITEM_BY_ID_SUCCESS:
return Object.assign([], state = action.item, {
item: action.item
});
default:
return state;
}
}
I have googled to get answers with no luck, I don't know where i made a mistake. If some one point out for me it would be a great help. Thanks in advance.
The reason you can't access the props in the constructor is that it is only called once, before the component is first mounted.
The action to load the item is called in the componentWillMount function, which occurs after the constructor is called.
It appears like you are trying to set a default value in the mapStateToProps function but aren't using it at all
function mapStateToProps(state, ownProps){
// this is never used
let item = {
itemTitle: ''
};
return {
item: state.itemReducer
};
}
The next part I notice is that your are taking the state from redux and trying to inject it into the component's local state
this.state = {
itemTitle: this.props.item.itemTitle,
errors: {}
};
Mixing redux state and component state is very rarely a good idea and should try to be avoided. It can lead to inconsistency and and hard to find bugs.
In this case, I don't see any reason you can't replace all the uses of this.state.itemTitle with this.props.items.itemTitle and remove it completely from the component state.
Observations
There are some peculiar things about your code that make it very difficult for me to infer the intention behind the code.
Firstly the reducer
export default function itemReducer(state = initialState.item, action) {
switch (action.type) {
case types.LOAD_ITEM_BY_ID_SUCCESS:
return Object.assign([], state = action.item, {
item: action.item
});
default:
return state;
}
}
You haven't shown the initialState object, but generally it represents the whole initial state for the reducer, so using initialState.item stands out to me. You may be reusing a shared initial state object for all of the reducers so I'm not too concerned about this.
What is very confusing the Object.assign call. I'm not sure it the intention is to output an object replacing item in the state, or if it is to append action.item to an array, or to have an array with a single item as the resulting state. The state = action.item part is also particularly puzzling as to it's intention in the operation.
This is further confused by the PropTypes for UpdateItem which requires item to be an array
UpdateItem.propTypes = {
item: PropTypes.array.isRequired
};
But the usage in the component treats it like and object
this.state = {
// expected some kind of array lookup here |
// V---------------
itemTitle: this.props.item.itemTitle,
errors: {}
};
Update from comments
Here is a example of what I was talking about in the comments. It's a simplified version of your code (I don't have all your components. I've also modified a few things to match my personal style, but hopefully you can still see what's going on.
In React Native and Redux, I am using <NavigationCardStack/> as the root component and render routes with _renderScene(). But seems like whenever the root component re-renders with state update, it does not pass the state down every update, because I put console.log(this.props) in the child component and logs the passed state, but it only logs once and that is the first time the app starts up and never logs after even if the root component re-renders with the state update.
Why isn't it passing down the updated state every time the state changes? And why doesn't the child component re-render whenever the root component does?
This is my set up:
_renderScene (props) {
const { route } = props.scene
return (
<route.component _handleNavigate={this._handleNavigate.bind(this)} state={this.props}/>
)
}
<NavigationCardStack
direction='horizontal'
navigationState={this.props.navigation}
onNavigate={this._handleNavigate.bind(this)}
renderScene={this._renderScene}
renderOverlay={this.renderOverlay}
style={styles.container}
/>
In _renderScene, props alone logs:
And this.props logs the actually state passed down via Redux:
And in the child component childPage.js, I am simply logging like so, and it logs the props passed down (_handleNavigate and state) correctly but the state just continues to represent initial state even if it gets updated:
render() {
console.log(this.props.state)
return (
Thank you in advance!
EDIT
This is my reducer and the child component would just log the initialState here even though other properties have been added and updated:
const initialState = {
meetUp: false,
}
function itemReducer(state = initialState, action) {
switch(action.type) {
case ITEM_QUANTITY:
return {
...state,
quantity: action.quantity
}
case ITEM_PRICE:
return {
...state,
price: action.price
}
case ITEM_MEET_UP:
return {
...state,
meetUp: action.meetUp
}
default:
return state
}
}
export default itemReducer
And connected to the root component like so:
function mapStateToProps(state) {
return {
itemInfo: state.itemReducer,
...
}
}
export default connect(
mapStateToProps,
{
itemQuantity: (value) => itemQuantity(value),
itemPrice: (value) => itemPrice(value),
itemMeetUp: (value) => itemMeetUp(value),
}
)(NavigationRoot)
With following actions:
export function itemMeetUp(value) {
return {
type: ITEM_MEET_UP,
meetUp: value
}
}
export function itemQuantity(value) {
return {
type: ITEM_QUANTITY,
quantity: value
}
}
export function itemPrice(value) {
return {
type: ITEM_PRICE,
price: value
}
}
There is only one reason that a child component does not render after a parent render - its shouldComponentUpdate method, or one from a component between it and the parent, has returned false. Often (particularly with Redux) shouldComponentUpdate methods are written to block the rerender if the properties haven't changed shallowly. Redux relies on you not mutating state, but instead always returning a new object with any changes from your reducer, otherwise the shouldComponentUpdate optimisation causes unexpected behaviour.
Could the problem be that you are modifying deep state rather than returning new objects? See http://redux.js.org/docs/basics/Reducers.html for more details.
A first guess is that your NavigationCardStack isn't rerendering since it thinks the props being presented to it is unchanged. Try forcing a rerender (forceUpdate) and see what happens.