I am rendering List of users, which I get from Redux store (I store it in variable data). Loading of users to Redux is async, that's why it takes a moment and my List has to re-render. I filter this data and save it in List's state and try to send it as prop to Table but it never passes state's value to my table's props.
const getData = data => {...}
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
displayData: [],
};
}
static getDerivedStateFromProps(props) {
return props.data ? { displayData: getData(props.data) } : null;
}
shouldComponentUpdate(nextProps) {
const { data } = this.props;
return data === undefined && data !== nextProps.data;
}
render() {
const { displayData } = this.state;
return data ? <Table displayData={displayData} /> : <h1>Loading...</h1>;
}
}
const mapStateToProps = ({ data }) => ({
data: data.users,
});
export default connect(mapStateToProps)(List);
Upper component:
class App extends React.Component {
componentDidMount() {
const { loadData, loadFromStorage } = this.props;
loadData();
}
render() {
return (
<Router history={history}>
<Header history={history} />
<Switch>
<Route path="/list" render={props => <List {...props} />} />
...
</Switch>
</Router>
);
}
}
}
const mapDispatchToProps = { loadData: LOAD_DATA };
export default connect(null, mapDispatchToProps)(App);
It just sets my List component's state and never passes any additional props to Table as they change (I use React extension and I checked props and state)
How can I fix this?
P.S.: I don't want this component to re-render every time I get new props
Try to remove "data === undefined", also return this.state.data !== nextState.data, do the below way:
shouldComponentUpdate(nextProps, nextState) {
return this.state.data !== nextState.data;
}
I don't know how you set up your reducer's default state for the data property on your reducer file. If the initial state for the data on your reducer file is {}, [], or null, your code will prevent the component from updating.
Also in your case, your table only takes data from state, not props. After state gets reset, this.props and nextProps may be equal already, so it is better to use this.state.
Related
I have a small react app. In App.js I have layout Sidenav and Content area. The side nav is shown on some page and hid from others. When I go to some components with sidenav, sidenav flag is set by redux and render the component again, in the componentDidMount I have api call, and it is executed twice.
class App extends Component {
renderSidebar = () => {
const {showNav} = this.props;
return showNav ? (
<TwoColumns>
<Sidenav/>
</TwoColumns>) : null;
};
render() {
const {showNav} = this.props;
const Column = showNav ? TenColumns : FullColumn;
return (
<Row spacing={0}>
{this.renderSidebar()}
<Column>
<Route exact path="/measurements/:id/:token/:locale/measure"
component={MeasurementPage}/>
</Column>
</Row>
)
}
}
const mapStateToProps = state => ({
showNav: state.sidenav.showNav
});
export default connect(mapStateToProps)(App);
I tried to use shouldComponentUpdate to prevent the second API call
class MeasurementPage extends Component {
constructor(props) {
// This update the redux "showNav" flag and re-render the component
props.toggleSidenav(false);
}
shouldComponentUpdate(nextProps) {
return !nextProps.showNav === this.props.showNav;
}
componentDidMount() {
// This is executed twice and made 2 api calls
this.props.getMeasurement(params);
}
render() {
return <h1>Some content here</h1>;
}
}
const mapStateToProps = state => ({
showNav: state.sidenav.showNav
});
export default connect(mapStateToProps)(MeasurementPage);
Did someone struggle from this state update and how manage to solve it?
This props.toggleSidenav(false) might cause side effect to your component lifecycle. We use to do this kind of stuff inside componentWillMount and it has been depreciated/removed for a reason :). I will suggest you move it inside componentDidMount
class MeasurementPage extends Component {
constructor(props) {
// This update the redux "showNav" flag and re-render the component
// props.toggleSidenav(false); // remove this
}
shouldComponentUpdate(nextProps) {
return nextProps.showNav !== this.props.showNav;
}
componentDidMount() {
if(this.props.showNav){ //the if check might not necessary
this.props.toggleSidenav(false);
this.props.getMeasurement(params);
}
}
render() {
return <h1>Some content here</h1>;
}
}
The comparison should be
shouldComponentUpdate(nextProps) {
return !(nextProps.showNav === this.props.showNav)
}
The problem is that !nextProps.showNav negate showNav value instead of negating the role expression value, and that is why you need an isolation operator.
It's No call twice anymore.
componentDidMount() {
if (this.first) return; this.first = true;
this.props.getMeasurement(params);
}
I am using React and Redux for a search application. Using react-router-dom, I'm routing /search/:term? to a Search component:
<Router>
<Switch>
<Route exact path="/search/:term?" component={Search} />
<Redirect to="/search" />
</Switch>
const Search = (props) => {
const { term } = props.match.params;
return (
<div>
<SearchForm term={term}/>
<SearchResults />
</div>
)
};
When a user submits a search in the SearchForm component, I'm dispatching an action to submit the search query. I'm also initiating a search in the constructor if a term is given, initially:
class SearchForm extends Component {
constructor(props) {
super(props);
const term = props.term ? props.term : '';
this.state = {
term: term,
}
if (term) {
this.props.submitSearch(term);
}
}
handleSubmit = (e) => {
e.preventDefault();
if (this.state.term) {
this.props.submitSearch(this.state.term);
}
}
render = () => {
<form
onSubmit={this.handleSubmit.bind(this)}>
...
</form>
}
}
I'm using withRouter from react-router-dom, so the URL updates when the search is submitted.
The problem happens when the user navigates Back in their browser. The URL navigates back, the props update (i.e. props.match.params.term), but the search does not resubmit. This is because the submitSearch action only gets dispatched in SearchForm.constructor (search on initial loading if a term is in the URL) and SearchForm.handleSubmit.
What is the best way to listen for a state change to term when the URL changes, then dispatch the search action?
I would retrieve the route parameter in componentDidMount since you are pushing a new route and therefore reloading the view.
Inside your SearchForm it would look like this.
state = {
term: '';
}
onChange = (term) => this.setState({ term })
onSubmit = () => this.props.history.push(`/search/${this.state.term}`);
And in your SearchResult :
componentDidMount() {
this.props.fetchResults(this.props.term)
}
A nice thing to do would be to keep the SearchResult component dry. There are several ways to achieve that, here is one using higher order components aka HOC :
export default FetchResultsHoc(Component) => {
#connect(state => ({ results: state.searchResults }))
class FetchResults extends React.Component {
componentDidMount(){
dispatch(fetchResults(this.props.match.params.term))
}
render(){
<Component {...this.props} />
}
}
return FetchResultsHoc;
}
That you would then call on your SearchResult component using a decorator.
import { fetchResults } from './FetchResultsHoc';
#fetchResults
export default class SearchResult extends React.PureComponent { ... }
// You have now access to this.props.results inside your class
My current solution is to dispatch submitSearch in the componentWillRecieveProps lifecycle method if the new props don't match the current props:
componentWillReceiveProps(nextProps) {
if (this.props.term !== nextProps.term) {
this.setState({
term: nextProps.term,
});
this.props.submitSearch(nextProps.term);
}
}
Then, instead of dispatching an action on form submission, I push a new location onto the history and componentWillReceiveProps does the dispatching:
handleSubmit = (e) => {
e.preventDefault();
if (this.state.term) {
this.props.history.push('/search/'+this.state.term);
}
}
This solution feels a little wrong, but it works. (Other's would seem to agree: Evil things you do with redux — dispatch in updating lifecycle methods)
Am I violating a React or Redux principle by doing this? Can I do better?
I am fetching data in parent 'wrapper' component and pass it down to two child components. One child component receives it well, another does not.
In container:
const mapStateToProps = createStructuredSelector({
visitedCountriesList: getVisitedCountriesList(),
visitedCountriesPolygons: getVisitedCountriesPolygons()
});
export function mapDispatchToProps(dispatch) {
return {
loadVisitedCountries: () => {
dispatch(loadVisitedCountriesRequest())
},
};
}
in redux-saga I fetch data from API and store them:
function mapPageReducer(state = initialState, action) {
switch (action.type) {
case FETCH_VISITED_COUNTRIES_SUCCESS:
return state
.setIn(['visitedCountriesPolygons', 'features'], action.polygons)
}
Selectors:
const getVisitedCountriesList = () => createSelector(
getMapPage,
(mapState) => {
let countriesList = mapState.getIn(['visitedCountriesPolygons', 'features']).map(c => {
return {
alpha3: c.id,
name: c.properties.name
}
});
return countriesList;
}
)
const getVisitedCountriesPolygons = () => createSelector(
getMapPage,
(mapState) => mapState.get('visitedCountriesPolygons')
)
in a wrapper component I render two components, triggering data fetch and passing props down to child components (visitedCountriesPolygons and visitedCountriesList):
class MapView extends React.Component {
constructor(props) {
super(props)
this.props.loadVisitedCountries();
}
render() {
return (
<div>
<Map visitedCountriesPolygons={this.props.visitedCountriesPolygons} />
<MapActionsTab visitedCountriesList={this.props.visitedCountriesList} />
</div>
);
}
}
Then, in first child component Map I receive props well and can build a map:
componentDidMount() {
this.map.on('load', () => {
this.drawVisitedPolygons(this.props.visitedCountriesPolygons);
});
};
But in the second component MapActionsTab props are not received at initial render, but only after any update:
class MapActionsTab extends React.Component {
constructor(props) {
super(props);
}
render() {
let countriesList = this.props.visitedCountriesList.map(country => {
return <li key={country.alpha3}>{country.name}</li>;
}) || '';
return (
<Wrapper>
<div>{countriesList}</div>
</Wrapper>
);
}
}
UPD:
Saga to fetch data form API:
export function* fetchVisitedCountries() {
const countries = yield request
.get('http://...')
.query()
.then((res, err) => {
return res.body;
});
let polygons = [];
yield countries.map(c => {
request
.get(`https://.../${c.toUpperCase()}.geo.json`)
.then((res, err) => {
polygons.push(res.body.features[0]);
})
});
yield put(fetchVisitedCountriesSuccess(polygons));
}
and a simple piece of reducer to store data:
case FETCH_VISITED_COUNTRIES_SUCCESS:
return state
.setIn(['visitedCountriesPolygons', 'features'], action.polygons)
Why is it different and how to solve it, please?
thanks,
Roman
Apparently, this works correct and it was just a minor issue in another place (not pasted here and not errors reported).
After thorough clean up and refactoring it worked as expected.
Conclusion: always keep your code clean, use linter and follow best practices :)
I think the problem may be in your selectors, in particular this one, whose component parts being executed immediately (with no fetched data values), and hence values will not change as it is memoized. This means that it will not cause an update to the component should the the underlying data change from the fetched data
const mapStateToProps = createStructuredSelector({
visitedCountriesList: getVisitedCountriesList, // should not execute ()
visitedCountriesPolygons: getVisitedCountriesPolygons // should not execute ()
});
By not executing the composed selectors immediately, mapStateToProps will call them each time the state changes and they should select the new values and cause an automatic update of your react component
I am using react-redux code structure and this is my first try with react-redux.I have cloned a github repository from Here and started editing it.
My directory structure:
Here schema is parent component and header and tables are 2 child components.Tables is showing data from localstorage through redux store.
Initializing store:
const initialState = JSON.parse(window.localStorage.getItem('test'));
const store = createStore(Reducers, initialState, compose(applyMiddleware(...middleware), extension));
Now an event is triggered from Header and sent to schema and in the response of this event schema is updating localstorage by requesting to server and saving server's response in localstorage as follows:
Schema.js:
class Schema extends PureComponent {
constructor(props) {
super(props);
this.table = '';
getTables();
}
myCallback = () => {//triggered by child(Header)
getTables();
}
getTables = () => {
axios.get(url)
.then((response) => {
if(response.data.status==0){
window.localStorage.setItem('test',JSON.stringify(response.data));
this.tables=JSON.parse(window.localStorage.getItem('test'))
});
}
render() {
console.log(this.tables);//this is giving updated value at each updation
return (
<div className='container-fluid'>
<Header callbackFromParent={ this.myCallback } />
<br />
<br />
<Tables val={ this.tables } />
</div>
);
}
}
Here is code for Tables.js:
class Tables extends Component {
props: Props
render() {
let {tables,val } = this.props;
console.log(JSON.parse(window.localStorage.getItem('test')));//this is giving updated value at each updation in localstorage
console.log(val);//this is also giving updated value at each updation in localstorage
tables=val;
console.log(tables);this is also updating in realtime.
return (
<div className='table-wrapper'>
{ tables.map((table) => (
<Table
key={ table.id }
data={ table }
/>
))}
</div>
);
}
}
type Props = {
tables: Array<TableType>
};
The issue is whenever header triggers callback, schema updates value of localstorage, this updation also re render Tables component. Also an updated value can be seen in render of Tables component but the tables which are shown are from previous saved value. To get current value in tables, we need to refresh the page.
is it a mistake in code flow or i need something else for this?
The idea is that react will trigger rendering of component whenever the component state or props is updated.
If the component props are updated in parent component you will still need to update the component state to make the next render in inner component
The key of this is using componentWillReceiveProps
I updated your code with the code below:
Basically i did the following:
1- I used component state for Schema, Tables, and Table
2- I used this.setState whenever i need to make updates to state (this is very important)
3- I make sure that when a component props is updated in parent i update the component state as well using componentWillReceiveProps and this will make the new render with updated data
Schema component:
class Schema extends Component {
constructor(props) {
super(props);
this.state = { tables : { } }
this.getTables = this.getTables.bind(this);
this.myCallback = this.myCallback.bind(this);
}
componentWillMount(){
this.getTables();
}
myCallback = () => {
//triggered by child(Header)
this.getTables();
}
getTables = () => {
axios.get(url)
.then((response) => {
if(response.data.status==0)
{
window.localStorage.setItem('test',JSON.stringify(response.data));
this.setState({
tables : JSON.parse(window.localStorage.getItem('test'))
});
}
);
}
render() {
//this is giving updated value at each updation
console.log(this.state.tables);
return (
<div className='container-fluid'>
<Header callbackFromParent={ this.myCallback } />
<br />
<br />
<Tables tables={this.state.tables} />
</div>
);
}
}
Tables Component
class Tables extends Component {
constructor(props) {
super(props);
this.state = { tables : { } }
}
componentWillMount(){
this.setState({
tables : this.props.tables
})
}
componentWillReceiveProps(nextProps){
this.setState({
tables : nextProps.tables
})
}
render() {
console.log(JSON.parse(window.localStorage.getItem('test')));//this is giving updated value at each updation in localstorage
console.log(this.state.tables);//this is also giving updated value at each updation in localstorage
return (
<div className='table-wrapper'>
{ this.state.tables.map((table) => (
<Table key={ table.id } data={ table } />
))
}
</div>
);
}
}
And finally a dummy Table component to show that you will also need to handle props update using componentWillReceiveProps to make sure each individual table component did render after props update
And probably this is where you have the issue ... because the tables are showing old data but the console.log of Tables component is logging new data which means Each individual Table component is not rending after the update
class Table extends Component {
constructor(props) {
super(props);
this.state = { data : { } }
}
componentWillMount(){
this.setState({
data : this.props.data
})
}
componentWillReceiveProps(nextProps){
this.setState({
data : nextProps.data
})
}
render() {
console.log(this.state.data);
return (
<table className='table'>
{this.state.data}
//use your table data here
</table>
);
}
}
Important Edit:
As mentioned by react documentation componentWillReceiveProps might get called even if the props have not changed, thats why in some situation you might need to consider comparing this.props with nextProps to make sure that you really got new updated props and based on that you update the component state ....
Is it possible?
I have a component where children are rendered by an arbitrary mapping function coming in as props. A simplified example:
class SomeComponent extends Component {
render() {
const { renderChild, businessObjects } = this.props
return <div>
{businessObjects.map(renderChild)}
</div>
}
}
I obviously get a warning saying children are rendered without the key attribute.
I tried assigning the key after the vdom element is rendered:
...
{
businessObjects.map(e => {
const vdom = renderChild(e)
vdom.key = e.id
return vdom
})
}
...
But the object returned from the JSX transform is frozen, so I can't do this. Also there is no API to temporarily unfreeze then re-freeze objects in js. Cloning is out of question for performance reasons (thousands of components are rendered like this)
What can I do?
Again, for performance reason I can't wrap the rendered children into another component, so a solution like this wouldn't work:
const Child = ({renderChild, bo}) => (<div>{renderChild(bo)}</div>)
// in SomeComponent
...
{
businessObjects.map(e => (<Child
key={e.id}
bo={e}
renderChild={renderChild}
/>)
)
}
...
Update
The reason for this structure is that SomeComponent is a dumb component, and has no access to application state (redux). But the rendered children do need to have access to dispatch (I do it in a form of connected action creators).
So you can imagine the whole thing like this:
const createChildRenderer = ({actionFoo, actionBar}) => (obj) => {
switch(obj.type) {
case FOO:
return <div onClick={() => actionFoo()}>{obj.text}</div>
case BAR:
return <div onClick={() => actionBar()}>{obj.text}</div>
default:
return null
}
}
And in a connected component
#connect(
({ businessObjects }) => { businessObjects },
{ actionFoo, actionBar}
)
class SmartComponent extends Component {
render() {
const renderChild = createChildRenderer({
actionFoo: this.props.actionFoo, // action creators
actionBar: this.props.actionBar
})
return (<SomeComponent
renderChild={renderChild}
businessObjects={this.props.businessObjects}>
}
}
The way I ended up solving this by taking an actual react component as an argument:
So that in the dumb component that previously took a renderer function, now I take a component:
class SomeComponent extends Component {
render() {
const { ChildComponent, businessObjects } = this.props
return <div>
{businessObjects.map((o) => (<ChildComponent
businessObject={o}
key={o.id}
/>)}
</div>
}
}
And where I previously created the renderer function, now I create the component:
const createChildComponent = ({actionFoo, actionBar}) =>
({ businessObject: obj }) => { // this is now a component created dynamically
switch(obj.type) {
case FOO:
return <div onClick={() => actionFoo()}>{obj.text}</div>
case BAR:
return <div onClick={() => actionBar()}>{obj.text}</div>
default:
return null
}
}
And in the connected component:
#connect(
({ businessObjects }) => { businessObjects },
{ actionFoo, actionBar}
)
class SmartComponent extends Component {
render() {
const ChildComponent = createChildComponent({
actionFoo: this.props.actionFoo, // action creators
actionBar: this.props.actionBar
})
return (<SomeComponent
ChildComponent={ChildComponent}
businessObjects={this.props.businessObjects}>
}
}
You can use cloneElement on the child received from renderChild:
React.cloneElement(
child,
{...child.props, key: yourKeyValue}
)