React: Build Connections Among Component - reactjs

I am a beginner of React JS. Messing around to achieve VanJS event listeners.
For layout, I decide to store things like a panel, a button as individual component.
Now, I have a component Movie and another component Button, how can I trigger state change of Movie by Button.onclick()?.
In other words, how to modify a component by the event happening on another component?
Before posting, I have read:
React Basics Component Lifecycle
Using the State Hook
but tons of method followed by really confused me.
useState
componentWillMount: immediately before initial rendering
componentDidMount: immediately after initial rendering
componentWillReceiveProps: when component receives new props
shouldComponentUpdate: before rendering, after receiving new props or state
componentWillUpdate: before rendering, after receiving new props or state.
componentDidUpdate: after component's updates are flushed to DOM
componentWillUnmount: immediately before removing component from DOM
Following is a demo, which contains a Movie card and a Button, when the button is clicked, I want the background colour of the Movie card to change
React TypeScript Code Sandbox
the code:
import * as React from "react";
import "./styles.css";
import styled from 'styled-components';
function CSSPropertiesToComponent(dict:React.CSSProperties){
let str = '';
for(const [key, value] of Object.entries(dict)){
let clo = '';
key.split('').forEach(lt=>{
if(lt.toUpperCase() === lt){
clo += '-' + lt.toLowerCase();
}else{
clo += lt;
}
});
str += clo + ':' + value + ';';
}
return str;
}
class Movie extends React.Component<any, any>{
public static style:React.CSSProperties|object = {
width: "300px",
height: "120px",
display: "flex",
justifyContent: "space-around",
alignItems: "center",
borderRadius: "20px",
filter: "drop-shadow(0px 1px 3px #6d6d6d)",
WebkitFilter: "drop-shadow(3px 3px 3px #6d6d6d)",
backgroundColor: '#'+Math.floor(Math.random()*16777215).toString(16),
fontSize: '2.5rem',
margin: '20px',
color: '#fff',
}
props:React.ComponentProps<any>;
state:React.ComponentState;
constructor(props:any) {
super(props);
this.props = props;
this.state = {style: Object.assign({}, Movie.style)}
this.changeColor = this.changeColor.bind(this);
}
changeColor():void{
this.setState({style: Object.assign({}, Movie.style, {backgroundColor: '#'+Math.floor(Math.random()*16777215).toString(16)})});
}
render():JSX.Element{
let StyledMovie = styled.div`
${CSSPropertiesToComponent(Movie.style)}
`;
return (
<>
<StyledMovie>{this.props.title}</StyledMovie>
</>
)
}
}
export default function App() {
let MV = new Movie({title: 'the Avengers'});
return (
<>
{MV.render()}
<button onClick={MV.changeColor}>Change Color</button>
</>
)
}
However, when clicking on the change colour button, it doesn't work and shows a warning:
Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the Movie component.
if anyone offers some suggestion, I will be so glad.

let MV = new Movie({title: 'the Avengers'});
return (
<>
{MV.render()}
<button onClick={MV.changeColor}>Change Color</button>
</>
)
Should look like (using jsx or tsx):
onChangeColor(color) {
setState({color: color}); // state won't be in Movie component !
}
// use React.Fragment if you don't want an additional div
return (
<div>
<Movie title={'the Avengers'} />
<button onClick={onChangeColor}>Change Color</button>
<div/>
)
Also on class Movie extends React.Component<any, any>, you are settings both generic parameters to any, the first one is the type of the "props" of the component (inputs and callbacks), and the second one is the type of the "state" of the component.
interface MovieProps {
// ...
}
interface MovieState {
// ...
}
class Movie extends React.Component<MovieProps, MovieState> { }
As said, looking at your code, you need to save the state outside Movie component, you might want to keep App component clean,and make another component to wrap Movie and button, that will be responsible to handle interaction between both.
There is a lot more to be said on this code, but I cannot cover all of it, I recommend you follow a tutorial to get stronger basics of React.

Related

How to use the React Context API?

I have a React App (setup with CRA) with a global element (custom cursor, that is a rounded div following the mouse) whose style I want to update/change when hovering various other components that are nested differently and more or less deeply (in the structure provided below I am only listing one example component). From what I understand this is a good use case for Context API.
The structure of my App looks like this (simplified):
<Cursor />
<Layout>
<Content>
<Item />
</Content>
</Layout>
So when hovering <Item /> (amongst other components) I want to update the style of the <Cursor /> component.
Therefore I tried to access a function I set up inside my <Cursor /> component in my <Item /> component. Unfortunately when hovering it does not update my state and as consequence the style of my <Cursor /> does not change.
My Cursor component looks like this (simplified):
import React, { Component } from "react"
export const CursorContext = React.createContext(false)
class Cursor extends Component {
constructor(props) {
super(props)
this.state = {
positionX: 0,
positionY: 0,
scrollOffsetY: 0,
display: "none",
isHoveringProjectTeaserImage: false,
}
this.handleMousePosition = this.handleMousePosition.bind(this)
this.handleMouseOverProjectTeaser = this.handleMouseOverProjectTeaser.bind(this)
this.handleMouseLeaveProjectTeaser = this.handleMouseLeaveProjectTeaser.bind(this)
}
handleMousePosition = (mouse) => {
this.setState({
positionX: mouse.pageX,
positionY: mouse.pageY,
display: "block",
scrollOffsetY: window.pageYOffset
})
}
handleMouseOverProjectTeaser = () => {
this.setState({
isHoveringProjectTeaserImage: true
})
}
handleMouseLeaveProjectTeaser = () => {
this.setState({
isHoveringProjectTeaserImage: false
})
}
componentDidMount() {
document.body.addEventListener("mousemove", this.handleMousePosition)
}
componentWillUnmount() {
document.body.removeEventListener("mousemove", this.handleMousePosition)
}
render() {
const {
positionX,
positionY,
display,
scrollOffsetY,
isHoveringProjectTeaserImage
} = this.state
return(
<CursorContext.Provider value={this.state}>
<div>
<StyledCursor
style={ isHoveringProjectTeaserImage
? {backgroundColor: "red", display: `${display}`, top: `${positionY - scrollOffsetY}px`, left: `${positionX}px`}
: {backgroundColor: "yellow", display: `${display}`, top: `${positionY - scrollOffsetY}px`, left: `${positionX}px`}}
/>
</div>
</CursorContext.Provider>
)
}
}
export default Cursor
And my Item Component that can be hovered looks like this (simplified):
import React, { Component } from "react"
import { CursorContext } from '../Cursor/Index';
class Item extends Component {
constructor(props) {
// not showing stuff in here that's not relevant
}
static contextType = CursorContext
render() {
return(
<CursorContext.Consumer>
{(value) =>
<StyledItem
onMouseOver={value.handleMouseOverProjectTeaser}
onMouseLeave={value.handleMouseLeaveProjectTeaser}
>
</StyledItem>
}
</CursorContext.Consumer>
)
}
}
export default Item
Do I even need to use static contextType = CursorContext?
When not passing a default value (I thought they are optional anyway) I am getting an TypeError: Cannot read property 'handleMouseOverProjectTeaser' of undefined, as soon as I pass a long a false as default value my App renders but does not update my <Cursor /> state.
Am I even using Context API correctly?
React.createContext default value?
As you correctly stated, the value passed to React.createContext() does not matter in this case.
When not passing a default value (I thought they are optional anyway) I am getting an TypeError: Cannot read property 'handleMouseOverProjectTeaser' of undefined, as soon as I pass a long a false as default value my App renders but does not update my state.
This brings out the fact that your default value is always used: try running undefined.blahblah vs. false.blahblah: the former throws a TypeError while the second silently returns undefined.
So we know the value you set in <Provider value={...}> never reaches the consumer, but why?
The Context is only available to its descendants
The <C.Consumer> is not rendered as a descendant of the <C.Provider>, so it can't get access to it. In other words, the provider should "enclose" the consumers. From the docs:
Context is designed to share data that can be considered “global” for a tree of React components [...]
The root of that tree is your <C.Provider>, and in your case, the consumer is not part of that tree.
Something like that could work:
<CursorContext>
<StyledCursor />
<Layout>
<Content>
<Item />
</Content>
</Layout>
</CursorContext>
Misc.
Do I even need to use static contextType = CursorContext?
Not really, if you're using <CursorContext.Consumer>. From the docs:
Context.Consumer: A React component that subscribes to context changes.
But in your case, since you don't need to listen to context changes (from your example code anyways), just keep the static contextType:
static contextType = CursorContext
render() {
return(
<StyledItem
onMouseOver={this.context.handleMouseOverProjectTeaser}
onMouseLeave={this.context.handleMouseLeaveProjectTeaser}
>
</StyledItem>
)
}
The point is you should use one or the other, you don't need both.
Last thing, you're passing this.state in the provider, and using this.context.handleMouseOverProjectTeaser in the child component... but there is no such function in <Cursor>'s state. Maybe you intended to pass <Cursor> itself, or better, just the handlers?

How to capture click outside React component

I started to learn React and I am trying to implement a modal window. I am at the same time using TypeScript.
I wanted to capture a click outside my React component, so when I click outside the modal window, this one closes. I based my approach on this: How to capture click outside React component
import styled from 'styled-components';
const StyledModal = styled.div`
width: 100%;
background-color: #fff;
box-shadow: 0 0 0.625rem, rgba(0, 0, 0, 0.2);
#media (min-width: 576px) {
width: 32rem;
},
`;
class Modal extends React.Component<ModalProps> {
private modal: HTMLDivElement;
onOutsideClick = (e: any) => {
if (!_.isNil(this.modal)) {
if (!this.modal.contains(e.target)) {
this.onClose(e);
}
}
}
componentDidMount() {
document.addEventListener('mousedown', this.onOutsideClick, false);
}
componentWillMount() {
document.removeEventListener('mousedown', this.onOutsideClick, false);
}
render() {
<div>
<StyledModal ref={(node: any) => { this.modal = node; }}>
...
</StyledModal>
</div>
}
}
The issue is whenever I click inside or outside the modal I get this error, which I don't know what it is or how to fix it:
Any lights please let me know...
Since your StyledModal is styled-components you need to add innerRef to be able to get the DOM node. Keep in mind innerRef is a custom prop only for styled-components
https://github.com/styled-components/styled-components/blob/master/docs/tips-and-tricks.md#refs-to-dom-nodes
<StyledModal innerRef={(node: any) => { this.modal = node; }}>
...
</StyledModal>
From styled-components v4 onward it is ref prop.
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
render() {
return (
<Input
ref={this.inputRef}
/>
);
}
For more info Refs
If you want to use a tiny component (466 Byte gzipped) that already exists for this functionality then you can check out this library react-outclick. It lets you capture clicks outside of a component.
The good thing about the library is that it also lets you detect clicks outside of a component and inside of another. It also supports detecting other types of events.

Enzyme testing with React/Redux - shallow rendering issues with setState

I have a React component that loads another component if it has it's initial local state altered. I can't get a clean test going because I need to set local state AND shallow render so the child component doesn't crash when the new component mounts because the redux store isn't there. It seems those two objectives are incompatible in Enzyme.
For the child component to display, things need to occur:
The component needs to receive a "response" props (any string will do)
The component need to have it's initial "started" local state updated to true. This is done with a button in the actual component.
This is creating some headaches with testing. Here is the actual line that determines what will be rendered:
let correctAnswer = this.props.response ? <div className="global-center"><h4 >{this.props.response}</h4><Score /></div> : <p className="quiz-p"><strong>QUESTION:</strong> {this.props.currentQuestion}</p>;
Here is my current Enzyme test:
it('displays score if response and usingQuiz prop give proper input', () => {
const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
wrapper.setState({ started: true })
expect(wrapper.contains(<Score />)).toEqual(true)
});
I am using shallow, because any time I use mount, I get this:
Invariant Violation: Could not find "store" in either the context or props of "Connect(Score)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(Score)".
Because the component is displayed through the parent, I can not simply select the disconnected version. Using shallow seems to correct this issue, but then I can not update the local state. When I tried this:
it('displays score if response and usingQuiz prop give proper input', () => {
const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
wrapper.setState({ started: true })
expect(wrapper.contains(<Score />)).toEqual(true)
});
The test fails because shallow doesn't let the DOM get updated.
I need BOTH conditions to be met. I can do each condition individually, but when I need to BOTH 1) render a component inside a component (needs shallow or will freak about the store not being there), and 2) update local state (needs mount, not shallow), I can't get everything to work at once.
I've looked at chats about this topic, and it seems this is a legitimate limitation of Enzyme, at least in 2017. Has this issue been fixed? It's very difficult to test this.
Here is the full component if anyone needs it for reference:
import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { Transition } from 'react-transition-group';
import { answerQuiz, deleteSession, getNewQuestion } from '../../actions/quiz';
import Score from '../Score/Score';
import './Quiz.css';
export class Quiz extends React.Component {
constructor(props) {
super(props);
// local state for local component changes
this.state = {
started: false
}
}
handleStart() {
this.setState({started: true})
}
handleClose() {
this.props.dispatch(deleteSession(this.props.sessionId))
}
handleSubmit(event) {
event.preventDefault();
if (this.props.correctAnswer && this.props.continue) {
this.props.dispatch(getNewQuestion(this.props.title, this.props.sessionId));
}
else if (this.props.continue) {
const { answer } = this.form;
this.props.dispatch(answerQuiz(this.props.title, answer.value, this.props.sessionId));
}
else {
this.props.dispatch(deleteSession(this.props.sessionId))
}
}
render() {
// Transition styles
const duration = 300;
const defaultStyle = {
opacity: 0,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
height: '100%',
width: '100%',
margin: '0px',
zIndex: 20,
top: '0px',
bottom: '0px',
left: '0px',
right: '0px',
position: 'fixed',
display: 'flex',
alignItems: 'center',
transition: `opacity ${duration}ms ease-in-out`
}
const transitionStyles = {
entering: { opacity: 0 },
entered: { opacity: 1 }
}
// Response text colors
const responseClasses = [];
if (this.props.response) {
if (this.props.response.includes("You're right!")) {
responseClasses.push('quiz-right-response')
}
else {
responseClasses.push('quiz-wrong-response');
}
}
// Answer radio buttons
let answers = this.props.answers.map((answer, idx) => (
<div key={idx} className="quiz-question">
<input type="radio" name="answer" value={answer} /> <span className="quiz-question-label">{answer}</span>
</div>
));
// Question or answer
let correctAnswer = this.props.response ? <div className="global-center"><h4 className={responseClasses.join(' ')}>{this.props.response}</h4><Score /></div>: <p className="quiz-p"><strong>QUESTION:</strong> {this.props.currentQuestion}</p>;
// Submit or next
let button = this.props.correctAnswer ? <button className="quiz-button-submit">Next</button> : <button className="quiz-button-submit">Submit</button>;
if(!this.props.continue) {
button = <button className="quiz-button-submit">End</button>
}
// content - is quiz started?
let content;
if(this.state.started) {
content = <div>
<h2 className="quiz-title">{this.props.title} Quiz</h2>
{ correctAnswer }
<form className="quiz-form" onSubmit={e => this.handleSubmit(e)} ref={form => this.form = form}>
{ answers }
{ button }
</form>
</div>
} else {
content = <div>
<h2 className="quiz-title">{this.props.title} Quiz</h2>
<p className="quiz-p">So you think you know about {this.props.title}? This quiz contains {this.props.quizLength} questions that will test your knowledge.<br /><br />
Good luck!</p>
<button className="quiz-button-start" onClick={() => this.handleStart()}>Start</button>
</div>
}
// Is quiz activated?
if (this.props.usingQuiz) {
return (
<Transition in={true} timeout={duration} appear={true}>
{(state) => (
<div style={{
...defaultStyle,
...transitionStyles[state]
}}>
{/* <div className="quiz-backdrop"> */}
<div className="quiz-main">
<div className="quiz-close" onClick={() => this.handleClose()}>
<i className="fas fa-times quiz-close-icon"></i>
</div>
{ content }
</div>
</div>
)}
</Transition >
)
}
else {
return <Redirect to="/" />;
}
}
}
const mapStateToProps = state => ({
usingQuiz: state.currentQuestion,
answers: state.answers,
currentQuestion: state.currentQuestion,
title: state.currentQuiz,
sessionId: state.sessionId,
correctAnswer: state.correctAnswer,
response: state.response,
continue: state.continue,
quizLength: state.quizLength,
score: state.score,
currentIndex: state.currentIndex
});
export default connect(mapStateToProps)(Quiz);
Here is my test using mount (this crashes due to a lack of store):
import React from 'react';
import { Quiz } from '../components/Quiz/Quiz';
import { Score } from '../components/Score/Score';
import { shallow, mount } from 'enzyme';
it('displays score if response and usingQuiz prop give proper input', () => {
const wrapper = mount(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
wrapper.setState({ started: true })
expect(wrapper.contains(<Score />)).toEqual(true)
});
});
This looks like a component that should be tested with mount(..).
How are you importing your connected component Score and Quiz?
I see that you are already correctly exporting Quiz component and default exporting the connected Quiz component.
Try importing with
import { Score } from '../Score/Score';
import { Quiz } from '../Quiz/Quiz';
in your test, and mount(..)ing. If you are importing from default export, you will get a connected component imported, which I think is the cause of the error.
Are you sure that Transition component let it's content to be displayed? I use this component and can't properly handle it in tests...
Can you for the test purposes alter your renders return with something like this:
if (this.props.usingQuiz) {
return (
<div>
{
this.state.started && this.props.response ?
(<Score />) :
(<p>No score</p>)
}
</div>
)
}
And your test can look something like this:
it('displays score if response and usingQuiz prop give proper input',() => {
const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
expect(wrapper.find('p').text()).toBe('No score');
wrapper.setState({ started: true });
expect(wrapper.contains(<Score />)).toEqual(true);
});
I also tested shallows setState and little test like this works fine:
test('HeaderComponent properly opens login popup', () => {
const wrapper = shallow(<HeaderComponent />);
expect(wrapper.find('.search-btn').text()).toBe('');
wrapper.setState({ activeSearchModal: true });
expect(wrapper.find('.search-btn').text()).toBe('Cancel');
});
So I believe that shallow properly handle setState and the problem caused by some components inside your render.
The reason you are getting that error is because you're trying to test the wrapper component generated by calling connect()(). That wrapper component expects to have access to a Redux store. Normally that store is available as context.store, because at the top of your component hierarchy you'd have a <Provider store={myStore} />. However, you're rendering your connected component by itself, with no store, so it's throwing an error.
Also, if you are trying to test a component inside a component, may full DOM renderer may be the solution.
If you need to force the component to update, Enzyme has your back. It offers update() and if you call update() on a reference to a component that will force the component to re-render itself.

Reactjs - passing state value from one component to another

I have two components SideNav and Dashboard (two are in different js files). SideNav will have selectbox as filters. I have to pass an array from Dashboard component to Sidebar component. This array has to given as values for select box (which is inside sidenav component).
P.S. What will be the case if I have two different component classes defined in two different JS files.
e.g. HomeComponent/Home.js -> Parent component
Dashboard/Dashboard.js -> Child component
I am making API call on "Home.js" file and getting some data. I want to pass these data to "Dashboard.js" file (component)
All the examples I studied, they show two components in the same JS file.
class Dashboard extends Component {
constructor(props) {
super(props);
this.state = {viz:{},filterData:{}};
}
var data1= ['1','2','3'];
this.setState({data1: data1}, function () {
console.log(this.state.data1);
});
}
//Sidebar
class Sidebar extends Component {
constructor(props) {
super(props);
this.state = {
data: ['opt1','opt2']
};
}
handleClick(e) {
e.preventDefault();
e.target.parentElement.classList.toggle('open');
this.setState({data: this.state.data1}, function () {
console.log(this.state.data);
});
}
render() {
const props = this.props;
const handleClick = this.handleClick;
return (
<div className="sidebar">
<nav className="sidebar-nav">
<Nav>
<li className="nav-item nav-dropdown">
<p className="nav-link nav-dropdown-toggle" onClick={handleClick.bind(this)}>Global</p>
<ul className="nav-dropdown-items">
<li> Organization <br/>
<select className="form-control">
<option value="">Select </option>
{this.state.data.map(function (option,key) {
return <option key={key}>{option}</option>;
})}
</select>
If you have to pass state from Dashboard to Sidebar, you have to render Sidebar from Dashboard's render function. Here, you can pass the state of Dashboard to Sidebar.
Code snippet
class Dashboard extends Component {
...
...
render(){
return(
<Sidebar data={this.state.data1}/>
);
}
}
If you want the changes made on props (data1) passed to Sidebar be received by Dashboard, you need to lift the state up. i.e, You have to pass a function reference from Dashboard to Sidebar. In Sidebar, you have to invoke it whenever you want the data1 to be passed back to Dashboard.
Code snippet.
class Dashboard extends Component {
constructor(props){
...
//following is not required if u are using => functions in ES6.
this.onData1Changed = this.onData1Changed.bind(this);
}
...
...
onData1Changed(newData1){
this.setState({data1 : newData1}, ()=>{
console.log('Data 1 changed by Sidebar');
})
}
render(){
return(
<Sidebar data={this.state.data1} onData1Changed={this.onData1Changed}/>
);
}
}
class Sidebar extends Component {
...
//whenever data1 change needs to be sent to Dashboard
//note: data1 is a variable available with the changed data
this.props.onData1changed(data1);
}
Reference Doc : https://facebook.github.io/react/docs/lifting-state-up.html
You can only pass props from parent to child component. Either restructure your components hierarchy to have this dependence, or use a state/event management system like Redux (react-redux) .
I had the same issue with parent and child components and the solution was simply send down the function (which is altering the state in the parent component) as a prop to the child component. In this way both are sharing that particular variable's state. Hope this straightforward approach helps you!
I believe keeping the status values aligned with the page URL is another good way, not only to pass values, but also to keep the page status controllable with urls.
Imagine that you are building an advanced search page, where different components will control the search criteria, hence, in addition to search functionality, user should be able to keep his search settings by the used URL.
Supposing that clicking on a link in component x adds a query string criteria1=x to the current page url, and so on for the other components. Let's say we have also configured the search functionality to depend on the URL to read state values from it, this way, you will be able to pass values from a specific component to any number of components without restrictions.
This is called as props drilling.
You can pass data from one component to another by several ways
useContext Hook
Context API
Redux (Its a pattern)
Context with useContext hook, this is a better approach as using Context will increase the code complexity
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background,
color: theme.foreground
}}> I am styled by theme context!</button> );
}
For detailed information you can visit the links : useContext , Context and Redux
Redux is better for large-scale application, and if you have multiple Context always go for useContext hook.

How should I be recreating a stateful child component in a parent's render method?

Facebook says that I should not keep a React component in its parent's state. Instead I should be recreating the child in render method each time it is run.
What Shouldn't Go in State?
React components: Build them in render() based on underlying props and
state.
Now my question is: How can I do that? Is it even possible? Isn't the state lost if I recreate a child component from scratch?
The only way I can think of that this scenario will work in, is that there's only one state object and it belongs to the root component. The rest of components will only have props and whenever they want to update some state of theirs, they need to call some parent's handler all the way up to root component, since it's the only component with an state object! And once updated, the root will give the child components back their state as props. Which I don't think it is practical at all!
[UPDATE]
Here's a sample code that I find hard not to store components in the parent's state:
http://codepen.io/mehranziadloo/pen/XdLvgq
class BlackBox extends React.Component
{
constructor() {
super();
this.state = {
counter: 0
};
}
increment() {
this.setState({ counter: this.state.counter+1 });
}
render() {
return (
<span onClick={this.increment.bind(this)} style={{
fontSize: '24pt',
border: '1px solid black',
margin: 10,
padding: 10,
}}>
{this.state.counter}
</span>
);
}
}
class RedBox extends React.Component
{
constructor() {
super();
this.state = {
counter: 0
};
}
increment() {
this.setState({ counter: this.state.counter+1 });
}
render() {
return (
<span onClick={this.increment.bind(this)} style={{
fontSize: '24pt',
border: '1px solid red',
margin: 10,
padding: 10,
}}>
{this.state.counter}
</span>
);
}
}
class Parent extends React.Component
{
constructor() {
super();
this.state = {
childCmps: [],
};
}
addBlackBox() {
let newState = this.state.childCmps.slice();
newState.push(<BlackBox key={newState.length} />);
this.setState({
childCmps: newState
});
}
addRedBox() {
let newState = this.state.childCmps.slice();
newState.push(<RedBox key={newState.length} />);
this.setState({
childCmps: newState
});
}
render() {
let me = this;
return (
<div>
<button onClick={this.addBlackBox.bind(this)}>Add Black Box</button>
<button onClick={this.addRedBox.bind(this)}>Add Red Box</button>
<br /><br />
{this.state.childCmps}
</div>
);
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
Isn't the state lost if I recreate a child component from scratch?
No, because React internally manages the backing instances (which hold the state) and does not replace them if two calls to render() say to render that component.
In other words:
ReactDOM.render(<MyComponent />, div);
ReactDOM.render(<MyComponent />, div);
This will not create MyComponent twice, but only once. It will render it twice: the first time it doesn't exist, so it creates it, and the second time it already exists, so it will update it. Any internal state that may be set between the two render passes will be preserved.
React is optimized to allow you to simply create complete, declarative render functions, and it figures out what changes are needed to actualize the rendering.
Update
The example you posted is using keys on a dynamic list of children. Keys are a way to identify specific children (and where they exist), so you need to be careful not to change keys between render passes for elements that maintain state.
Instead of storing the actual rendered components in state, such as <BlackBox key={i} />, store the necessary data to render the component, such as the component class BlackBox and a unique identifier for the key. (FYI you shouldn't use index as key, since index can change. I recommend using an always incrementing counter.)
Here is the Parent class modified to work without storing rendered components in state (the other components can remain as is):
class Parent extends React.Component {
static blackCount = 0;
static redCount = 0;
state = {
childCmps: [],
};
constructor(props, context) {
super(props, context);
}
addBlackBox = () => {
this.setState({
childCmps: [...this.state.childCmps, { Component: BlackBox, id: "black" + (++Parent.blackCount) }]
});
};
addRedBox = () => {
this.setState({
childCmps: [...this.state.childCmps, { Component: RedBox, id: "red" + (++Parent.redCount) }]
});
};
render() {
return (
<div>
<button onClick={this.addBlackBox}>Add Black Box</button>
<button onClick={this.addRedBox}>Add Red Box</button>
<br /><br />
{this.state.childCmps.map(child => <child.Component key={child.id} />)}
</div>
);
}
}
Example in CodePen.
Notes:
I used static (aka global) props to count how many black and red boxes have been added, combined with the strings "red" and "black" to form unique keys. (You can use Parent.blackCount = 0, etc, to initialize static class properties if you don't have support for class properties.)
I used fat arrow function properties as event handler callbacks to ensure this is in the correct scope. (You can use this.addBlackBox = this.addBlackBox.bind(this) in the constructor if you don't have support for class properties.)
I moved state initialization to a class property. As you can guess, I highly recommend you make use of class properties. :)
I used ES6 spread with array literal initialization to append a new box and create a new array.
Finally, in the Parent/render() function each box component is always re-rendered using a map() of the state with dynamic component type rendering of <child.Component>.
You only need to keep in parent's state any data necessary for rendering the children components. Typically, this is just the props you want pass, or the type of component.
In your case, this is just the color of the component "Red" or "Black".
So in parent state, an array containing Strings with value "Red" or "Black" is enough.
And everytime one of the buttons is clicked, you simply add another item to the array, and set state again. Something like this.
addRedBox() {
let newChildList = this.state.childList.slice();
newChildList.push("Red");
this.setState({
childList: newChildList
});
}
And then in your render() function do this:
{this.state.childList.map(function(color,i) {
if (color=="Black") {
return <BlackBox key={i} />
} else {
return <RedBox key={i} />
}
})}
On re-render, you simply pass new props (if any) to your child components, and each child component will then also re-render with the new props.
Passing new props to the child will not reset the child component. It will simply run all lifecycle methods again (including render()).
You can find a working version in this codepen.
the component only renders if the state changes(updates), and you should keep your state simple, use props to communicate with the children components.
and when your App gets larger you can use Flux or Redux to manage your states
You are attempting to see an Object-Oriented approach in React. Don't. There's OO and then there's whatever it is that Facebook do.
No, you cannot store components in state, as per the documentation you quoted. You can try it but you'll find things just don't work.
Here's an example of an OO class (in pseudocode):
class Parent {
list children
temporarilyAbondonChildren() {
for each child in children {
Orphanage.leaveAtDoorStep( child )
}
doStuffForTenYears()
for each child in Children {
output child.isOk()
}
}
}
And here's the closest equivalent to it in React:
class Parent {
temporarilyAbandonChildren() {
doStuffForTenYears()
child_properties = Orphanage.whatWouldveHappenedToMyKidHadIGivenBirthAndLeftThemForTenYears()
children = renderImaginaryChildren( child_properties )
for each child in children {
output child.isOk()
}
}
}
The only way I can think of that this scenario will work in, is that there's only one state object and it belongs to the root component. The rest of components will only have props and whenever they want to update some state of theirs, they need to call some parent's handler all the way up to root component, since it's the only component with an state object! And once updated, the root will give the child components back their state as props. Which I don't think it is practical at all!
I agree.

Resources