I'm new to React, and I'd like to ask a strategy question about how best to accomplish a task where data must be communicated between sibling components.
First, I'll describe the task:
Say I have multiple <select> components that are children of a single parent that passes down the select boxes dynamically, composed from an array. Each box has exactly the same available options in its initial state, but once a user selects a particular option in one box, it must be disabled as an option in all other boxes until it is released.
Here's an example of the same in (silly) code. (I'm using react-select as a shorthand for creating the select boxes.)
In this example, I need to disable (ie, set disabled: true) the options for "It's my favorite" and "It's my least favorite" when a user selects them in one select box (and release them if a user de-selects them).
var React = require('react');
var Select = require('react-select');
var AnForm = React.createClass({
render: function(){
// this.props.fruits is an array passed in that looks like:
// ['apples', 'bananas', 'cherries','watermelon','oranges']
var selects = this.props.fruits.map(function(fruit, i) {
var options = [
{ value: 'first', label: 'It\'s my favorite', disabled: false },
{ value: 'second', label: 'I\'m OK with it', disabled: false },
{ value: 'third', label: 'It\'s my least favorite', disabled: false }
];
return (
<Child fruit={fruit} key={i} options={options} />
);
});
return (
<div id="myFormThingy">
{fruitSelects}
</div>
)
}
});
var AnChild = React.createClass({
getInitialState: function() {
return {
value:'',
options: this.props.options
};
},
render: function(){
function changeValue(value){
this.setState({value:value});
}
return (
<label for={this.props.fruit}>{this.props.fruit}</label>
<Select
name={this.props.fruit}
value={this.state.value}
options={this.state.options}
onChange={changeValue.bind(this)}
placeholder="Choose one"
/>
)
}
});
Is updating the child options best accomplished by passing data back up to the parent through a callback? Should I use refs to access the child components in that callback? Does a redux reducer help?
I apologize for the general nature of the question, but I'm not finding a lot of direction on how to deal with these sibling-to-sibling component interactions in a unidirectional way.
Thanks for any help.
TLDR: Yes, you should use a props-from-top-to-bottom and change-handlers-from-bottom-to-top approach. But this can get unwieldy in a larger application, so you can use design patterns like Flux or Redux to reduce your complexity.
Simple React approach
React components receive their "inputs" as props; and they communicate their "output" by calling functions that were passed to them as props. A canonical example:
<input value={value} onChange={changeHandler}>
You pass the initial value in one prop; and a change handler in another prop.
Who can pass values and change handlers to a component? Only their parent. (Well, there is an exception: you can use the context to share information between components, but that's a more advanced concept, and will be leveraged in the next example.)
So, in any case, it's the parent component of your selects that should manage the input for your selects. Here is an example:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
// keep track of what is selected in each select
selected: [ null, null, null ]
};
}
changeValue(index, value) {
// update selected option
this.setState({ selected: this.state.selected.map((v, i) => i === index ? value : v)})
}
getOptionList(index) {
// return a list of options, with anything selected in the other controls disabled
return this.props.options.map(({value, label}) => {
const selectedIndex = this.state.selected.indexOf(value);
const disabled = selectedIndex >= 0 && selectedIndex !== index;
return {value, label, disabled};
});
}
render() {
return (<div>
<Select value={this.state.selected[0]} options={this.getOptionList(0)} onChange={v => this.changeValue(0, v)} />
<Select value={this.state.selected[1]} options={this.getOptionList(1)} onChange={v => this.changeValue(1, v)} />
<Select value={this.state.selected[2]} options={this.getOptionList(2)} onChange={v => this.changeValue(2, v)} />
</div>)
}
}
Redux
The main drawback of the above approach is that you have to pass a lot of information from the top to the bottom; as your application grows, this becomes difficult to manage. React-Redux leverages React's context feature to enable child components to access your Store directly, thus simplifying your architecture.
Example (just some key pieces of your redux application - see the react-redux documentation how to wire these together, e.g. createStore, Provider...):
// reducer.js
// Your Store is made of two reducers:
// 'dropdowns' manages the current state of your three dropdown;
// 'options' manages the list of available options.
const dropdowns = (state = [null, null, null], action = {}) => {
switch (action.type) {
case 'CHANGE_DROPDOWN_VALUE':
return state.map((v, i) => i === action.index ? action.value : v);
default:
return state;
}
};
const options = (state = [], action = {}) => {
// reducer code for option list omitted for sake of simplicity
};
// actionCreators.js
export const changeDropdownValue = (index, value) => ({
type: 'CHANGE_DROPDOWN_VALUE',
index,
value
});
// helpers.js
export const selectOptionsForDropdown = (state, index) => {
return state.options.map(({value, label}) => {
const selectedIndex = state.dropdowns.indexOf(value);
const disabled = selectedIndex >= 0 && selectedIndex !== index;
return {value, label, disabled};
});
};
// components.js
import React from 'react';
import { connect } from 'react-redux';
import { changeDropdownValue } from './actionCreators';
import { selectOptionsForDropdown } from './helpers';
import { Select } from './myOtherComponents';
const mapStateToProps = (state, ownProps) => ({
value: state.dropdowns[ownProps.index],
options: selectOptionsForDropdown(state, ownProps.index)
}};
const mapDispatchToProps = (dispatch, ownProps) => ({
onChange: value => dispatch(changeDropdownValue(ownProps.index, value));
});
const ConnectedSelect = connect(mapStateToProps, mapDispatchToProps)(Select);
export const Example = () => (
<div>
<ConnectedSelect index={0} />
<ConnectedSelect index={1} />
<ConnectedSelect index={2} />
</div>
);
As you can see, the logic in the Redux example is the same as the vanilla React code. But it is not contained in the parent component, but in reducers and helper functions (selectors). An instead of top-down passing of props, React-Redux connects each individual component to the state, resulting in a simpler, more modular, easier-to-maintain code.
The following help me to setup communication between two siblings. The setup is done in their parent during render() and componentDidMount() calls.
class App extends React.Component<IAppProps, IAppState> {
private _navigationPanel: NavigationPanel;
private _mapPanel: MapPanel;
constructor() {
super();
this.state = {};
}
// `componentDidMount()` is called by ReactJS after `render()`
componentDidMount() {
// Pass _mapPanel to _navigationPanel
// It will allow _navigationPanel to call _mapPanel directly
this._navigationPanel.setMapPanel(this._mapPanel);
}
render() {
return (
<div id="appDiv" style={divStyle}>
// `ref=` helps to get reference to a child during rendering
<NavigationPanel ref={(child) => { this._navigationPanel = child; }} />
<MapPanel ref={(child) => { this._mapPanel = child; }} />
</div>
);
}
}
Related
I'm relatively new to react and am having a little trouble understanding passing props/states from a child to parent component. I've managed to implement something that works but it is extremely laggy.
Overview: I am trying to create a form with multiple Material UI TextFields, with a submission button at the end to submit the form.
My approach: I am using state to update individual textfields on their inputs, and then dynamically updating another state in the parent FormSection file. Once the user clicks on the 'Submit' Button (not implemented yet), it will then take all the states from the parent class.
Existing Classes:
CompanyField.js
JobTitle.js
FormSection.js (Main File)
Implementation Appearance
CompanyField Class (will be same for Job Title, etc):
const Root = styled('div')(({ theme }) => ({
}));
class CompanyField extends React.Component {
state = { company: '' }
handleOnChange = (event) => {
event.preventDefault();
this.setState({ company: event.target.value.toLowerCase() });
this.props.onCompanyChange(this.state.company);
}
render() {
return (
<Root>
<Box
noValidate
autoComplete="off"
>
<TextField
id = "outlined-basic"
label = "Company"
variant = "outlined"
fullWidth
value = {this.state.company}
onChange = { this.handleOnChange }
/>
</Box>
</Root>
);
}
}
export default CompanyField;
Index Class
class FormSection extends React.Component {
state = { company: '', jobTitle: '' }
onCompanyUpdate = (value) => {
this.setState({company: value})
// console.log('Company:', this.state.company);
}
render() {
return (
<FormContainer>
<FormContentHeaderWrapper>
<FormContentHeader>
Company & Job Information
</FormContentHeader>
</FormContentHeaderWrapper>
<FormWrapperFull>
<CompanyField onCompanyChange={ this.onCompanyUpdate } />
<JobTitleField onJobTitleChange={ this.onJobTitleUpdate } />
</FormWrapperFull>
</FormContainer>
)
}
Could someone explain whether I am doing this the correct way? Else, would there be a better way to resolve this state passing method?
Thanks in advance!
When you update the parent component the whole tree re-renders on every keystroke. In your case your component very small it should not be a big impact. There are three approaches in my mind.
first of all, you have to use react developer tools for investigating further re-renders and finding the real problem.
first: You might use form validation libraries. For example; "react hook forms"
second: You might use React's "React.memo" function to memorize components.
third: You might use refs for input value management. You add values to ref and when you need them you iterate that ref object. You don't update the state. If there is no state update there will be no rerender.
for example:
In parent component:
const values = useRef({description: "", jobtitle: ""});
const onChange(name, value) {
values.current[name] = value;
}
In child component: (it must be an "uncontrolled component")
const handleCompanyChange = (evt) => {
const value = evt.target.value;
const name = "company";
props.onChange(name, value);
}
I have a very simple component with a text field and a button:
It takes a list as input and allows the user to cycle through the list.
The component has the following code:
import * as React from "react";
import {Button} from "#material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}
This component works great, except I have not handled the case when the state changes. When the
state changes, I would like to reset the currentNameIndex to zero.
What is the best way to do this?
Options I have conciderred:
Using componentDidUpdate
This solution is ackward, because componentDidUpdate runs after render, so I need to add a clause
in the render method to "do nothing" while the component is in an invalid state, if I am not careful,
I can cause a null-pointer-exception.
I have included an implementation of this below.
Using getDerivedStateFromProps
The getDerivedStateFromProps method is static and the signature only gives you access to the
current state and next props. This is a problem because you cannot tell if the props have changed. As
a result, this forces you to copy the props into the state so that you can check if they are the same.
Making the component "fully controlled"
I don't want to do this. This component should privately own what the currently selected index is.
Making the component "fully uncontrolled with a key"
I am considering this approach, but don't like how it causes the parent to need to understand the
implementation details of the child.
Link
Misc
I have spent a great deal of time reading You Probably Don't Need Derived State
but am largely unhappy with the solutions proposed there.
I know that variations of this question have been asked multiple times, but I don't feel like any of the answers weigh the possible solutions. Some examples of duplicates:
How to reset state in a component on prop change
Update component state when props change
Updating state on props change in React Form
Appendix
Solution using componetDidUpdate (see description above)
import * as React from "react";
import {Button} from "#material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
if(this.state.currentNameIndex >= this.props.names.length){
return "Cannot render the component - after compoonentDidUpdate runs, everything will be fixed"
}
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>): void {
if(prevProps.names !== this.props.names){
this.setState({
currentNameIndex: 0
})
}
}
}
Solution using getDerivedStateFromProps:
import * as React from "react";
import {Button} from "#material-ui/core";
interface Props {
names: string[]
}
interface State {
currentNameIndex: number
copyOfProps?: Props
}
export class NameCarousel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { currentNameIndex: 0}
}
render() {
const name = this.props.names[this.state.currentNameIndex].toUpperCase()
return (
<div>
{name}
<Button onClick={this.nextName.bind(this)}>Next</Button>
</div>
)
}
static getDerivedStateFromProps(props: Props, state: State): Partial<State> {
if( state.copyOfProps && props.names !== state.copyOfProps.names){
return {
currentNameIndex: 0,
copyOfProps: props
}
}
return {
copyOfProps: props
}
}
private nextName(): void {
this.setState( (state, props) => {
return {
currentNameIndex: (state.currentNameIndex + 1) % props.names.length
}
})
}
}
As i said in the comments, i'm not a fan of these solutions.
Components should not care what the parent is doing or what is the current state of the parent, they should simply take in props and output some JSX, this way they are truly reusable, composable and isolated which also makes testing a lot easier.
We can make the NamesCarousel component hold the names of the carousel together with the functionality of the carousel and the current visible name and make a Name component which does only one thing, display the name that comes in through props
To reset the selectedIndex when the items are changing add a useEffect with items as a dependency, although if you just add items to the end of the array you can ignore this part
const Name = ({ name }) => <span>{name.toUpperCase()}</span>;
const NamesCarousel = ({ names }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
setSelectedIndex(0)
}, [names])// when names changes reset selectedIndex
const next = () => {
setSelectedIndex(prevIndex => prevIndex + 1);
};
const prev = () => {
setSelectedIndex(prevIndex => prevIndex - 1);
};
return (
<div>
<button onClick={prev} disabled={selectedIndex === 0}>
Prev
</button>
<Name name={names[selectedIndex]} />
<button onClick={next} disabled={selectedIndex === names.length - 1}>
Next
</button>
</div>
);
};
Now this is fine but is the NamesCarousel reusable? no, the Name component is but the Carousel is coupled with the Name component.
So what can we do to make it truly reusable and see the benefits of designing component in isolation?
We can take advantage of the render props pattern.
Lets make a generic Carousel component which will take a generic list of items and invoke the children function passing in the selected item
const Carousel = ({ items, children }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
setSelectedIndex(0)
}, [items])// when items changes reset selectedIndex
const next = () => {
setSelectedIndex(prevIndex => prevIndex + 1);
};
const prev = () => {
setSelectedIndex(prevIndex => prevIndex - 1);
};
return (
<div>
<button onClick={prev} disabled={selectedIndex === 0}>
Prev
</button>
{children(items[selectedIndex])}
<button onClick={next} disabled={selectedIndex === items.length - 1}>
Next
</button>
</div>
);
};
Now what this pattern actually gives us?
It gives us the ability to render the Carousel component like this
// items can be an array of any shape you like
// and the children of the component will be a function
// that will return the select item
<Carousel items={["Hi", "There", "Buddy"]}>
{name => <Name name={name} />} // You can render any component here
</Carousel>
Now they are both isolated and truly reusable, you can pass items as an array of images, videos, or even users.
You can take it further and give the carousel the number of items you want to display as props and invoke the child function with an array of items
return (
<div>
{children(items.slice(selectedIndex, selectedIndex + props.numOfItems))}
</div>
)
// And now you will get an array of 2 names when you render the component
<Carousel items={["Hi", "There", "Buddy"]} numOfItems={2}>
{names => names.map(name => <Name key={name} name={name} />)}
</Carousel>
Can you use a functional component? Might simplify things a bit.
import React, { useState, useEffect } from "react";
import { Button } from "#material-ui/core";
interface Props {
names: string[];
}
export const NameCarousel: React.FC<Props> = ({ names }) => {
const [currentNameIndex, setCurrentNameIndex] = useState(0);
const name = names[currentNameIndex].toUpperCase();
useEffect(() => {
setCurrentNameIndex(0);
}, names);
const handleButtonClick = () => {
setCurrentIndex((currentNameIndex + 1) % names.length);
}
return (
<div>
{name}
<Button onClick={handleButtonClick}>Next</Button>
</div>
)
};
useEffect is similar to componentDidUpdate where it will take an array of dependencies (state and prop variables) as the second argument. When those variables change, the function in the first argument is executed. Simple as that. You can do additional logic checks inside of the function body to set variables (e.g., setCurrentNameIndex).
Just be careful if you have a dependency in the second argument that gets changed inside the function, then you will have infinite rerenders.
Check out the useEffect docs, but you'll probably never want to use a class component again after getting used to hooks.
You ask what is the best option, the best option is to make it a Controlled component.
The component is too low in the hierarchy to know how to handle it's properties changing - what if the list changed but only slightly (perhaps adding a new name) - the calling component might want to keep the original position.
In all cases I can think about we are better off if the parent component can decide how the component should behave when provided a new list.
It's also likely that such a component is part of a bigger whole and needs to pass the current selection to it's parent - perhaps as part of a form.
If you are really adamant on not making it a controlled component, there are other options:
Instead of an index you can keep the entire name (or an id component) in the state - and if that name no longer exists in the names list, return the first in the list. This is a slightly different behavior than your original requirements and might be a performance issue for a really really really long list, but it's very clean.
If you are ok with hooks, than useEffect as Asaf Aviv suggested is a very clean way to do it.
The "canonical" way to do it with classes seems to be getDerivedStateFromProps - and yes that means keeping a reference to the name list in the state and comparing it. It can look a bit better if you write it something like this:
static getDerivedStateFromProps(props: Props, state: State = {}): Partial<State> {
if( state.names !== props.names){
return {
currentNameIndex: 0,
names: props.names
}
}
return null; // you can return null to signify no change.
}
(you should probably use state.names in the render method as well if you choose this route)
But really - controlled component is the way to go, you'll probably do it sooner or later anyway when demands change and the parent needs to know the selected item.
Having a bit of a hard time finding anything about this specific pattern, and I'm not even exactly sure how to describe it in search terms so apologies if this is a duplicate question (although I don't think it is).
I want to output a React layout based on an order passed to the app that the user can set via a settings panel. The idea is that there are a few different containers to output on the page that I want the user to be able to re-arrange. It's important to note that this order is not changeable after the app renders. (I want the user to be able to say "Show me PanelA, PanelC and PanelB in that order")
Now, I've figured out how to accomplish this using the following pattern:
// user-ordered array is passed to the app:
const settings = {
layout: [
"panela",
"panelb",
"panelc"
]
}
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
// some state
}
}
renderComponentsInOrder() {
return settings.layout.map(component => {
switch(component) {
case "panela":
return
<PanelA {.../* some state */} />
case "panelb":
return
<PanelB {.../* some state */} />
case "panelc":
return
<PanelC {.../* some state */} />
default: return null
}
})
}
render() {
return this.renderComponentsInOrder()
}
}
but this strikes me as really inefficient. The order of the components shouldn't need to be re-calculated every time render runs because the order won't change while the app is running. I've tried doing things like memoizing the renderComponentsInOrder method to cache the calculated layout or renaming the values in the settings.layout array and calling them directly, but haven't been able to get anything to work because the components need to update based on state.
Any help or advice would be greatly appreciated. Thanks!
EDIT: Ideally I'm looking for a JS-based solution as compatibility is a bit of an issue and I don't want to rely solely on the browser's implementation of CSS.
A slightly different take on the answer given by Brandon. You could have a function to generate the component based on the state/props:
const settings = {
layout: [
"panela",
"panelb",
"panelc"
]
};
const panels = settings.layout.map(c => {
switch (c) {
case "panela": return (props, state) => <PanelA key="a" foo={state.foo} />
case "panelb": return (props, state) => <PanelB key="b" bar={state.bar} />
}
});
// Now use panels array to render:
class MyComponent extends React.Component {
render() {
const props = this.props;
const state = this.state;
const ordered = panels.map(p => p(props, state));
return <div>{ordered}</div>
}
}
Method 1:
Just transform the settings array into an array of Components once before you render:
const settings = {
layout: [
"panela",
"panelb",
"panelc"
]
};
const panels = settings.layout.map(c => {
switch (c) {
case "panela": return { Component: PanelA, key: c };
case "panelb": return { Component: PanelA, key: c };
...
}
});
// Now use panels array to render:
class MyComponent extends React.Component {
// ...
renderComponentsInOrder() {
return panels.map(({Component, key}) => (
<Component key={key} {.../* some state*/} />
));
}
// ...
}
Method 2:
Just create a mapping table:
const settings = {
layout: [
"panela",
"panelb",
"panelc"
]
};
const panelLookup = {
"panela": PanelA,
"panelb": PanelB,
...
};
// Now use lookup to render:
// Now use panels array to render:
class MyComponent extends React.Component {
// ...
renderComponentsInOrder() {
return settings.layout.map(key => {
const Component = panelLookup[key];
return <Component key={key} {.../* some state*/} />;
});
}
// ...
}
Say I have a functional React presentation component, like so:
const Functional = (props) => {
// do some stuff
return (
<div>
// more HTML based on the props
</div>
);
}
Functional.propTypes = {
prop1: React.PropTypes.string.isRequired,
prop2: React.PropTypes.string.isRequired,
// ...
};
If I'm using Redux and following the container component pattern, what would be the best way to render a dynamic number of these <Functional/> components inside a wrapper component, based on elements inside a array (which is inside my Redux state)?
E.g. My Redux state might look like this:
{
functionalItems: [
{
prop1: 'x1',
prop2: 'y1',
// ...
},
{
prop1: 'x2',
prop2: 'y2'
},
// ... more items
],
// ...
}
So each item in the functionalItems array should correspond to a <Functional/> component, which all get rendered adjacent to each other.
This is the second time I have come across this problem, so I'm hoping that it's common enough that there is good solution out there.
I'll post the solutions I can come up with (but which have undesirable traits), as answers to this question.
I'd like to suggest that you pass the entire array to the wrapper component like this:
const mapStateToProps = (state) => ({
items: getFunctionalItems(state),
// ...
});
and then in your Wrapper.jsx, do it like this:
const Wrapper = (props) => {
const elements = props.items.map((item, index) => {
<Functional prop1={ item.prop1 } prop2={ item.prop2 } ...
key={ ... /* you can use index here */ }/>
});
return (
<div>
{ elements }
</div>
);
};
...where getFunctionalItems() is an accessor function that is the canonical means of accessing the functional items from the state.
This way, you can handle changes in state structure, or a different rendering layout. (ergo more robust (I think)). And it looks more like following the Single Responsibility Principle.
Solution Description
Create a wrapper functional presentation component that takes in a quantity and functions to fetch the <Functional/> prop values.
Create a container component that connects to this new wrapper component and passes in the quantity (based on the Redux state) and accessor functions to fetch the <Functional/> prop values.
Example Code
Wrapper.jsx:
const Wrapper = (props) => {
const elements = [];
for (let i = 0; i < props.quantity; i++) {
elements.push(
<Functional prop1={ getPropValue1(i) } prop2={ getPropValue2(i) } ...
key={ ... }/>
);
}
return (
<div>
{ elements }
</div>
);
};
Wrapper.propTypes = {
quantity: React.PropTypes.number.isRequired,
getPropValue1: React.PropTypes.func.isRequired,
getPropValue2: React.PropTypes.func.isRequired,
// ...
};
ContainerComponent.js:
const mapStateToProps = (state) => ({
quantity: state.functionalItems.length,
getPropValue1: (index) => state.functionalItems[index].prop1,
getPropValue2: (index) => state.functionalItems[index].prop2,
// ...
});
const ContainerComponent = connect(mapStateToProps)(Wrapper);
I'm having trouble figuring out how to short circuit rendering a branch
of a tree of React components using Immutable.js cursors.
Take the following example:
import React from 'react';
import Immutable from 'immutable';
import Cursor from 'immutable/contrib/cursor';
let data = Immutable.fromJS({
things: [
{title: '', key: 1},
{title: '', key: 2}
]
});
class Thing extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.thing.deref() !== nextProps.thing.deref();
}
handleChangeTitle(e) {
this.props.thing.set('title', e.target.value);
}
render() {
return <div>
<input value={this.props.thing.get('title')}
onChange={this.handleChangeTitle.bind(this)} />
</div>;
}
}
class Container extends React.Component {
render() {
const cursor = Cursor.from(this.props.data, 'things', newThings => {
data.set('things', newThings);
renderContainer();
});
const things = cursor.map(thing => (
<Thing thing={thing} key={thing.get('key')} />
));
return <div>
{things}
</div>;
}
}
const renderContainer = () => {
React.render(<Container data={data} />, document.getElementById('someDiv'));
};
Say I change the first Thing's title. Only the first Thing will render with
the new title and the second Thing will not re-render due to
shouldComponentUpdate. However, if I change the second Thing's title, the
first Thing's title will go back to '' since the second Thing's cursor
is still pointing at an older version of the root data.
We update the cursors on each render of Container but the ones that don't
render due to shouldComponentUpdate also don't get the new cursor with the updated
root data. The only way I can see keeping the cursors up to date is to remove
shouldComponentUpdate in the Thing component in this example.
Is there a way to change this example to use shouldComponentUpdate using fast referential
equality checks but also keep the cursors updated?
Or, if that's not possible, could you provide an overview of how you would generally work with cursors + React components and rendering only components with updated data?
I updated your code, see comments inline:
class Thing extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.thing.deref() !== nextProps.thing.deref();
}
handleChangeTitle(e) {
// trigger method on Container to handle update
this.props.onTitleChange(this.props.thing.get('key'), e.target.value);
}
render() {
return <div>
<input value={this.props.thing.get('title')}
onChange={this.handleChangeTitle.bind(this)} />
</div>;
}
}
class Container extends React.Component {
constructor() {
super();
this.initCursor();
}
initCursor() {
// store cursor as instance variable to get access from methods
this.cursor = Cursor.from(data, 'things', newThings => {
data = data.set('things', newThings);
// trigger re-render
this.forceUpdate();
});
}
render() {
const things = this.cursor.map(thing => (
<Thing thing={thing} key={thing.get('key')} onTitleChange={this.onTitleChange.bind(this)} />
));
return <div>
{things}
</div>;
}
onTitleChange(key, title){
// update cursor to store changed things
this.cursor = this.cursor.update(x => {
// update single thing
var thing = x.get(key - 1).set('title', title);
// return updated things
return x.set(key - 1,thing);
});
}
}
const renderContainer = () => {
React.render(<Container data={data} />, document.getElementById('someDiv'));
};