Laggy TextField Updates in React - javascript

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);
}

Related

Which of these strategies is the best way to reset a component's state when the props change

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.

Testing in React/Redux - how to guarantee that state has been updated?

I'm writing tests for my new React app, part of my intention with this project is to fully understand this testing thing - it's been on my radar for a while but I haven't put it into production before.
Written a fair number of test so far that are using snapshots and other static & synchronous approaches. This seems to work fine until now where I'm dealing with a setState -> expect(postFunctionState).toEqual(desiredState) situation and while I console.log my way through the flow and can see that setState() is being called and I can see the results in the browser, I can't seem to write a test that replicates the behaviour.
Here's the relevant code:
//Component (extracted):
export class CorsetCreator extends React.Component {
constructor(props) {
super(props);
this.state = {
productName: '',
productType: 'Overbust',
enabled: false,
created: false,
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleNameChange = this.handleNameChange.bind(this);
this.handleProductChange = this.handleProductChange.bind(this);
}
handleNameChange(e) {
this.setState({ productName: e.target.value });
this.handleChange.bind(this)(e);
}
handleProductChange(e) {
this.setState({ productType: e.target.value });
this.handleChange.bind(this)(e);
}
handleChange(e) {
e.preventDefault();
if (e.target.value === '') {
this.setState({ enabled: false }); //User should not be able to submit an empty product name
return;
}
const { corsets } = this.props.corsetGallery;
if (!corsets) {
this.forceUpdate();
this.setState({ enabled: true }); //Point of this exercise is to guarantee unique combos of name&type. If there are no pre-existing corsets then no need to test for uniqueness
return;
}
const productType =
e.target.value === 'Underbust' || e.target.value === 'Overbust'
? e.target.value
: this.state.productType;
const productName =
e.target.value === 'Underbust' || e.target.value === 'Overbust'
? this.state
: e.target.value;
const filteredCorsets = corsets.filter(
corset => corset.type === productType && corset.name === productName,
);
this.setState({
enabled: !(filteredCorsets && filteredCorsets.length > 0),
});
}
//Test (extracted)
it('handles statechanges correctly with a valid new corset', () => {
const store = configureStore({}, browserHistory);
const creator = mount(
<Provider store={store}>
<CorsetCreator />
</Provider>,
);
const namebox = creator.find('NameBox').at(0);
const nameBoxField = namebox.find('input').at(0);
const submitbutton = creator.find('SubmitButton').at(0);
creator.setState({ enabled: false });
expect(submitbutton.props().enabled).toEqual(false);
nameBoxField.simulate('change', { target: { value: 'Test' } });
creator.update();
expect(creator.state().enabled).toEqual(true);
});
Because setState is asynchronous I feel like some sort of callback or promise may be the solution here but I've tried both and can't seem to sort through the best way. What is the best way to think about this type of scenario?
TL;DR: Remember React components are functions. In all their glory, they take in props and you receive an output of the render() function. Test the output.
If you have a variable in the state, chances are you're passing it to a child component or manipulating the visual output of the current component. Or said item in state would be useless :)
Testing the state is redundant, as it's like testing React itself.
A concern one normally raises is "But I'm showing/hiding that element by using setState(...)", or "I'm passing the state down into one of the children as a prop".
When writing tests, render the component. Simulate an action and check if the output of the render function has changed.
Take this component for example:
class TextWithClick extends React.Component {
state={ count: 0 };
handleClick = () => {
this.setState({ count: this.state.count + 1})
}
render() {
return (
<input
value={this.state.count} {/* state passed down as props */}
onClick={this.handleClick}
/>
)
}
}
ReactDOM.render(<TextWithClick/>
, document.getElementById('root'))
It's simple. Clicking on the input field, increases the text it shows, which is a prop. Here are some test assertions:
// using enzyme
it("should increase the count", () => {
const wrapper = shallow(<TextWithClick />);
const input = wrapper.find("input").at(0);
// test props
input.simulate("click");
expect(input.props().value).toEqual(1);
input.simulate("click");
expect(input.props().value).toEqual(2);
input.simulate("click");
expect(input.state().count).toEqual(3); // this tests React more than your component logic :)
});
Remember React components are functions. In all their glory, they take in props and you receive an output of the render() function. Test the output.
In the case of Redux, same thing. Testing state change is like testing Redux's connect() functionality. Mozilla uses a real redux store to test their app. I.e. test the final output.
I quote from the above link regarding testing a React/Redux app (can't seem to to multi-line blockquotes in SO:
"We dispatch real Redux actions to test application state changes. We test each component only once using shallow rendering.
"We resist full DOM rendering (with mount()) as much as possible.
"We test component integration by checking properties.
"Static typing helps validate our component properties.
"We simulate user events and make assertions about what action was dispatched.
A good article: Testing React Component’s State by Anthony Ng.

React TypeScript - Component properties not being updated when parent state is changed

This issue is in React using TypeScript
I have an array of components (Text inputs) sitting in my state. I also have an array of strings in my state that define the placeholder text of each component. Each component refers to the array of placeholder string from the state, at its corresponding index.
When I update a string within the placeholders array (using setState), the components do not update.
Where am I going wrong? Or have I misunderstood how states/props work.
Thank you!
I have simplified the code a lot, to highlight the issue.
Code:
interface IState {
inputComponents: IResponseOption[];
test_placeholder: string;
}
interface IResponseOption {
Component: JSX.Element;
InputValue: string;
}
class NewMultiQuestion extends React.Component<IProps, IState> {
constructor(props: any){
super(props);
if (this.props.location.props) {
this.state = {
inputComponents: [],
test_placeholder: "TESTING"
}
}
}
componentDidMount() {
this.generateInputElems();
}
generateInputElems() {
var createdInputOptions: IResponseOption[] = [];
for (var i = 0; i < 5; i++) {
var newInput: IResponseOption = {
Component: (
<div key={i}>
<TextInput key={i} id={i + ""} value="" placeholder={this.state.test_placeholder} hoverText={this.state.test_placeholder} />
</div>
),
InputValue: ""
}
createdInputOptions.push(newInput);
}
this.setState({inputComponents: createdInputOptions});
}
changeState() {
this.setState({test_placeholder: "Did it change?!"});
}
public render() {
let responseInputs = Object.keys(this.state.inputComponents).map((key) => {
return (this.state.inputComponents[key].Component);
});
return (
<div>
{responseInputs}
</div>
);
}
}
export default NewMultiQuestion;
Firstly, the input elements are only generated when the component is mounted. They aren't re-built when the state updates, you'd have to call generateInputElems() again after changing test_placeholder in the state. That's not ideal, since state changes shouldn't be in response to other state changes, but to things like user action or responses from API calls.
Secondly, you shouldn't be storing entire components in the state. Just the store the data needed to render them, then build the components during the render, e.g.
render() {
return(
<div>
{this.state.inputComponents.map((input, i) => (
<div key={i}>
<TextInput id={i + ""} value="" placeholder={this.state.test_placeholder} hoverText={this.state.test_placeholder} />
</div>
))}
</div>
);
}
That way when the placeholder state value changes, they'll be re-rendered with the new placeholder prop value.
Also for arrays of components only the containing element needs the key. And it's usually not a good idea to use the array index as the key, if the list changes (elements added/removed) then you'll get buggy behaviour if the key is the index. Best to find some unique value to identify each array element.

React contentEditable and cursor position

I have simple component
class ContentEditable extends React.Component {
constructor(props) {
super(props);
this.handleInput = this.handleInput.bind(this);
}
handleInput(event) {
let html = event.target.innerHTML;
if (this.props.onChange && html !== this.lastHtml) {
this.props.onChange({ target: { value: html, name: this.props.name } });
this.lastHtml = html;
}
}
render() {
return (
<span
contentEditable="true"
onInput={this.handleInput}
className={"auto " + this.props.className}
dangerouslySetInnerHTML={{ __html: this.props.value }}
/>
);
}
}
export default ContentEditable;
<ContentEditable
value={this.state.name}
onChange={e => {
this.setState({ name: e.target.value });
}}
/>;
The component works but the cursor position never changes, it is always on first position instead after the rendered text.
I tested examples form this forum but it doesn't work for me.
I use React 15.6.1 and test it on chrome (Os X).
Any hint how I can solve this problem?
The solution with useRef will be something look like below.
Here the useRef will keep the default value / initial value apart from the component rendering cycles, so it will retain the original value without being affected by other kinds of operations we do in the react component.
This component does two things
This will emit the user input to the parent component with an onChange method
Takes a default value from parent component as prop named value and renders the value in the custom input box (that was created using contentEditable)
I have added a code sandbox, link here, use this to see how this works!
The code sandbox example contains two components
one is ContentEditableWithRef which solves the problem with useRef , which is an uncontrolled component and
the other component is ContentEditable which uses useState to solve the same problem.
I also had same problem. Just fixed it with ref. Just assign textContent of event.target to ref.
const textareaEl = useRef<HTMLDivElement>(null);
const handleChange = (e: React.ChangeEvent<HTMLDivElement>) => {
textareaEl.current.textContent = e.target.textContent;
onChange(e); // If you have change event for form/state
};
/** If you're passing value from state,
you can mutate it each change for not losing cursor position.
*/
useEffect(() => {
if (value) {
textareaEl.current.textContent = value;
}
}, [value]);
return (
<div
id="textarea-element"
ref={textareaEl}
contentEditable={true}
suppressContentEditableWarning={true}
onChange={handleChange}
/>
)

React.js - Communicating between sibling components

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>
);
}
}

Categories