I'm loading some react components on demand (among with other information) depending on user input.
The components to render are kept in an array and the render method uses array.map to include the components.
The problem is, that if I trigger a forceUpdate() of the main app component, the mapped components won't update.
Code example: https://codesandbox.io/s/react-components-map-from-array-ekfb7
The dates are not updating because you are creating the instance of the component in your add function, and from then on you are referencing that instance without letting react manage the updates.
This is why storing component instances in state or in other variables is an anti-pattern.
Demonstration of the problem
Below I've created a working example still using forceUpdate just to prove what the issue is.
Notice instead of putting the component in state, I'm just pushing to the array to increase it's length. Then React can manage the updates correctly.
class TestComponent extends React.Component {
render() {
return <p>{Date.now()}</p>;
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.comps = [1];
}
add() {
this.comps.push(1);
this.forceUpdate();
}
render() {
return (
<div className="App">
<h1>Components map example</h1>
<p></p>
<h2>Static TestComponent (ok):</h2>
<TestComponent />
<h2>TestComponents mapped from an array (not ok):</h2>
{this.comps.map((comp, id) => {
return <div key={id}><TestComponent /></div>;
})}
<h2>All should update when the App component renders</h2>
<p>
<button onClick={() => this.add()}>Add TestComponent</button>
<button onClick={() => this.forceUpdate()}>forceUpdate App</button>
</p>
</div>
);
}
}
ReactDOM.render(<App/>,document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
This is still a less than ideal solution. But it does show where the issue lies.
A better solution
If you need to know more about each component instance up front, you can make the array more complex.
I would also suggest using state to store the comps array, and removing forceUpdate completely.
class TestComponent extends React.Component {
render() {
return <p>{Date.now()} {this.props.a} {this.props.b}</p>;
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
comps: [{ a: 'a', b: 'b' }]
}
}
add = () => {
// add your custom props here
this.setState(prev => ({comps: [ ...prev.comps, { a: 'c', b: 'd' } ]}));
}
render() {
return (
<div className="App">
<h1>Components map example</h1>
<p></p>
<h2>Static TestComponent (ok):</h2>
<TestComponent />
<h2>TestComponents mapped from an array (not ok):</h2>
{this.state.comps.map((compProps, id) => {
return <div key={id}><TestComponent {...compProps} /></div>;
})}
<h2>All should update when the App component renders</h2>
<p>
<button onClick={() => this.add()}>Add TestComponent</button>
</p>
</div>
);
}
}
ReactDOM.render(<App/>,document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Now notice that each component in the map callback can have it's own unique set of props based on whatever logic you what. But the parts that should re-render will do so correctly.
In order to update in React, you have to put your data in the state and then setState.
setState() schedules an update to a component’s state object. When state changes, the component responds by re-rendering which means updating the screen with the new state.
import React from "react";
import "./styles.css";
class TestComponent extends React.Component {
render() {
return <p>{Date.now()}</p>;
}
}
export class App extends React.Component {
constructor(props) {
super(props);
this.state = {
comps: [<TestComponent />],
}
}
add = () => {
this.setState({ comps: this.state.comps.concat(<TestComponent />) })
}
render() {
return (
<div className="App">
<h1>Components map example</h1>
<p></p>
<h2>Static TestComponent (ok):</h2>
<TestComponent />
<h2>TestComponents mapped from an array (not ok):</h2>
{
this.state.comps.map((comp, id) => {
return <div key={id}>{comp}</div>;
})
}
<h2>All should update when the App component renders</h2>
<p>
<button onClick={this.add}>Add TestComponent</button>
</p>
</div>
);
}
}
Related
I want add some behavior on a given lifecycle method of a React application without having to define it in every one of them?
I came from Java world and have been trying to use HOC for printing/console at every react component life cycle methods similar to AOP concept in Spring/Javaor you can say universal cross cutting on life cycle methods like componentWillUnmount, componentDidMount, componentWillMount... I want to console component name and lifecyle methods name.
Example
Component B componentWillMount called
Component A componentWillMount called
Component B componentDidMountcalled ...
I have tried to use HOC correct me if am wrong but it seems I will be forced to pass all components through this function.
Extend React lifecycle hook (e.g add a print statement on every ComponentDidMount) similar question was asked before but the solution only prints the parent components life cycle but not the child?
Thank you for your help and I really appreciate if you include a code snippet.
The only solution in React that I can think of would be to create a new base class that extends the base React Component class. Add the lifecycle method into the base class and then every component you create extends this new class if you want it to use the lifecycle method.
class NewBaseClass extends React.Component {
componentDidMount() {
console.log('do something on mount')
}
}
class CustomComponent extends NewBaseClass {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
I havent tested this exact use case let me know if it helps :)
You can implement an HOC similar to the following:
function withLifeCycleLogs(WrappedComponent) {
const Enhanced = class extends React.Component {
componentDidMount() {
console.log(`Component ${WrappedComponent.name} did mount`);
}
componentDidUpdate(prevProps) {
console.log(`Component ${WrappedComponent.name} did update`, {
// Uncomment below lines to inspect props change
// prevProps,
// nextProps: this.props
});
}
componentWillUnmount() {
console.log(`Component ${WrappedComponent.name} will unmount`);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
// Wrap the display name for easy debugging
// https://reactjs.org/docs/higher-order-components.html#convention-wrap-the-display-name-for-easy-debugging
Enhanced.displayName = `WithLifeCylceLogs${getDisplayName(WrappedComponent)}`;
// Static Methods Must Be Copied Over
// https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over
//
// hoistNonReactStatic(Enhanced, WrappedComponent);
return Enhanced;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || "Component";
}
class Counter extends React.Component {
render() {
return (
<div>
<button onClick={this.props.increment}>Increment</button>
<p>{this.props.counter}</p>
<button onClick={this.props.unmount}>Unmount Counter</button>
</div>
);
}
}
const CounterContainer = withLifeCycleLogs(Counter);
class App extends React.Component {
constructor() {
super();
this.state = {
counter: 0,
counterVisible: true
};
}
increment = () => {
this.setState((state) => ({ ...state, counter: state.counter + 1 }));
};
unmountCounter = () => {
this.setState((state) => ({ ...state, counterVisible: false }));
};
render() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{this.state.counterVisible && (
<CounterContainer
counter={this.state.counter}
increment={this.increment}
unmount={this.unmountCounter}
/>
)}
</div>
);
}
}
const AppContainer = withLifeCycleLogs(App);
const rootElement = document.getElementById("root");
ReactDOM.render(
<AppContainer />,
rootElement
);
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<div id="root">
</div>
Yes, you need to pass every component to withLifeCycleLogs function. But this is really simple and does not use much space, check this out:
class AwesomeComponent extends React.Component {
//
}
export default withLifeCylceLogs(AwesomeComponent);
It's like using annotation in Spring (correct me if I'm wrong)
CodeSandbox
I need to implement a kind of Master/Detail View for a Web Application in React. Since the app should be integrated into a CakePHP app I can't use React Router for handling the routes (since CakePHP would process them).
I have a List of Items and want to navigate through them, showing a Detail View. Items are nested, so there're SubItems to navigate to.
For now I got a ItemList Component, showing a list of Cards with a clickhandler. How can I change the View without changing the url?
ItemList Component looks like:
class ItemList extends React.Component {
constructor(props) {
super(props);
this.state = {
itemList: []
}
}
componentDidMount() {
fetchItems(...)
}
render() {
return(
<div>
{this.state.itemList.map(item => (
<Item key={item.id} item={item} />
))}
</div>
);
}
}
Item Component looks like:
class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
item: props.item,
}
}
handleClick = () => {
// How to navigate to another Component?
}
render() {
return(
<div>
<div className="card my-2" onClick={this.handleClick}>
<div className="card-body">
<h5 className="card-title">{this.state.item.title}</h5>
<p className="card-text">{this.state.item.description}</p>
</div>
</div>
</div>
);
}
}
Thanks in advance!
You should have a parent component (let's say MainView) that has a state (let's say selectedItemId).
class MainView extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedItemId: [null]
}
}
componentDidMount() {
}
render() {
return(
{!selectedItemId && (<ItemList />)}
{selectedItemId && (
<ItemDetail id={selectedItemId} />
)}
);
}
}
As you can see, it renders different components based on the selectedItemId state value.
Inside the ItemList handleClick you call the setState of the parent MainView to set the selected item ID.
So using conditional rendering inside the render() function of MainView you can render the ItemList when no item is selected and ItemDetail when you have selected one.
I'm not really used to ES6 syntax components so my code can be wrong somewhere, but you can get the message ;)
I'm having trouble with lifting state up and converting components to functions. What's wrong with my code.
Instructions: 1: Inside the JS section, create a class component named App. Inside its render() method, have it return the Welcome component. In the ReactDOM.render() method, change Welcome to App.
2: Lift the state from the Welcome component to App, so that the state is initialized inside of App's constructor.
3: Convert the Welcome component to a function component that returns the same welcome message as before. You will need to pass the bootcampName property of state from App to the Welcome component. It's up to you whether or not to destructure it.
class App extends Component {
constructor(props) {
super(props);
this.state = {
bootcampName: "Nucamp"
};
}
render() {
return (
<div className="App">
<Welcome {this.state.bootcampName}>;
</div>
);
};
}
function Welcome(props) {
return (
<h1>Welcome to {this.props.bootcampName}!</h1>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
You have some errors there
You need to close the Welcome Component.
You need to name the prop
Destruct the props in because of this.state do not exist there.
Here the Code:
class App extends Component {
constructor(props) {
super(props);
this.state = {
bootcampName: "Nucamp"
};
}
render() {
return (
<div className="App">
{ /**
* you need to close the Welcome Component
* you need to name the prop
*/}
<Welcome bootcampName={this.state.bootcampName}/>;
</div>
);
};
}
// Here destruct props to use it
function Welcome({bootcampName}) {
return (
<h1>Welcome to {bootcampName}!</h1>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
Name the prop:
<div className="App">
<Welcome bootcampName={this.state.bootcampName} />
</div>
When you use functional components, you no longer need to use this. Try doing this instead:
class App extends Component {
constructor(props) {
super(props);
this.state = {
bootcampName: "Nucamp"
};
}
render() {
return (
<div className="App">
<Welcome bootcampName={this.state.bootcampName}>;
</div>
);
}
}
function Welcome({bootcampName}) {
return (
<h1>Welcome to {bootcampName}!</h1>
);
}
I have two components, one parent one child. I am using the fetch method in componentDidMount() callback. Once I do this, I set the state with key items to that data that is pulled from the api. Once I do this it should be able to be console logged in the child component as a prop. However this is not working. What am I doing wrong here?
Parent Component:
import React, {Component} from 'react';
import Map from './maps/Map';
class Main extends Component {
constructor(props){
super(props);
this.state = {
name: "John",
items: []
}
}
componentDidMount() {
fetch('https://hn.algolia.com/api/v1/search?query=')
.then(dat => dat.json())
.then(dat => {
this.setState({
items: dat.hits
})
})
}
render() {
return (
<div>
<Map list={this.state.name} items={this.state.items}></Map>
</div>
)
}
}
export default Main;
Child Component:
import React, {Component} from 'react';
class Map extends Component {
constructor(props) {
super(props);
console.log(props.items)
}
render () {
return (
<h1>{this.props.name}</h1>
)
}
}
export default Map;
First, fetch is asynchronous. So, the fetch statement might be pending by the time you try to console.log the result inside the child constructor.
Putting the console.log inside the render method would work, because the component will be rerendered, if the state items changes.
The constructor for a component only runs one time during a lifecycle. When it does, props.items is undefined because your ajax request is in-flight, so console.log(props.items) doesn't show anything.
If you change your constructor to console.log("constructed");, you'll see one-time output (stack snippets may not show this--look in your browser console). Henceforth, componentDidUpdate() can be used to see the new props that were set when your ajax request finishes.
You could also log the props inside the render method, which will run once before the ajax request resolves and again afterwards when props.items changes.
As a side point, you have <Map list=... but the component tries to render this.props.name, which is undefined.
Also, if you aren't doing anything in the constructor (initializing state or binding functions) as here, you don't need it.
class Map_ /* _ added to avoid name clash */ extends React.Component {
constructor(props) {
super(props);
console.log("constructed");
}
componentDidUpdate(prevProps, prevState) {
const props = JSON.stringify(this.props, null, 2);
console.log("I got new props", props);
}
render() {
return (
<div>
<h1>{this.props.name}</h1>
<pre>
<ul>
{this.props.items.map((e, i) =>
<li key={i}>{JSON.stringify(e, null, 2)}</li>)}
</ul>
</pre>
</div>
);
}
}
class Main extends React.Component {
constructor(props) {
super(props);
this.state = {name: "John", items: []};
}
componentDidMount() {
fetch('https://hn.algolia.com/api/v1/search?query=')
.then(dat => dat.json())
.then(dat => {
this.setState({items: dat.hits})
});
}
render() {
return (
<div>
<Map_
name={this.state.name}
items={this.state.items}
/>
</div>
);
}
}
ReactDOM.createRoot(document.querySelector("#app"))
.render(<Main />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
The only problem you have is that you are trying to use this.props.name and your Map component props are called list and items, so it will return undefined.
If you log your props in the constructor you will get the initial state of Main because the fetch hasn't returned anything yet. Remember that the constructor only runs once. So you are probably getting an empty array when you log props.items in the constructor because that's what you have in your initial state.
{
name: "John",
items: []
}
If you log the props in your render method you will see your array filled with the data you fetched, as you can see here:
https://codesandbox.io/s/stoic-cache-m7d43
If you don't want to show the component until the data is fetched you can include a boolean property in your state that you set to true once you the fetch returns a response and pass it as a prop to your component. Your component can you use that variable to show, for example, a spinner while you are fetching the data. Here's an example:
https://codesandbox.io/s/reverent-edison-in9w4
import CircularProgress from "#material-ui/core/CircularProgress"
class Main extends Component {
constructor(props) {
super(props);
this.state = {
name: "John",
items: [],
fecthed: false
};
}
componentDidMount() {
fetch("https://hn.algolia.com/api/v1/search?query=")
.then(dat => dat.json())
.then(dat => {
this.setState({
items: dat.hits,
fecthed: true
});
});
}
render() {
return (
<Map
fetched={this.state.fecthed}
list={this.state.name}
items={this.state.items}
/>
);
}
}
class Map extends Component {
render() {
return (
<div>
{this.props.fetched ? (
<div>
<h1>{this.props.list}</h1>
{this.props.items.map((item, indx) => (
<div key={indx}>Author: {item.author}</div>
))}
</div>
) : (
<CircularProgress />
)}
</div>
);
}
}
Hope this helps. Cheers!
I'm trying to better understand the role of keys in React components. I've read quite a bit but every example I've seen (like the one in the React docs or the great explanation on S.O.) assumes the data coming into the component is dynamic.
The examples all apply keys with array index values or using something like .map() to assign database IDs dynamically to each instance of the child component, and satisfy React's need for keys.
My example is on a static site with static content coming into the child component that gets called a couple of times. Best I figured, I could create a random number generator function getRandomInt and apply the key that way.
Unfortunately this results in the familiar React error:
Each child in an array or iterator should have a unique "key" prop.
Check the render method of CaseStudyOpinionSummary. It was passed a
child from DiagnosticCaseStudy.
Where am I going wrong?
Parent component (DiagnosticCaseStudy)
import React from 'react'
import CaseStudyOpinionSummary from '../../../components/CaseStudyOpinionSummary'
export default class DiagnosticCaseStudy extends React.Component {
getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
render() {
return (
<CaseStudyOpinionSummary
part="Part One"
partTitle="Diagnosis"
partSubtitle="Primary Care Encounter"
partSummary="Short brief"
key={ this.getRandomInt(0, 100000) }
/>
<CaseStudyOpinionSummary
part="Part Two"
partTitle="Medication and Management"
partSubtitle="Initial Gastroenterologist Encounter"
partSummary="Another short brief"
key={ this.getRandomInt(0, 100000) }
/>
)
}
Child component (CaseStudyOpinionSummary)
import React from 'react'
export default class CaseStudyOpinionSummary extends React.Component {
render() {
return (
<div>
<section className="lightest-gray-bg">
<section className="aga-cs-container-short">
<section className="aga-container">
<h2 className="aga-cs-orange-title">{[this.props.part, ": ", this.props.partTitle ]}</h2>
<h2 className="aga-cs-question-title">{ this.props.partSubtitle }</h2>
{ this.props.partSummary }
</section>
</section>
</section>
</div>
)
}
}
React only needs the key prop to distinguish between sibling components in an array. You don't need the key prop for regular sibling components.
class AppWithArray extends React.Component {
render() {
return (
<div>
{[
<div key="1"> test1 </div>,
<div key="2"> test2 </div>
]}
</div>
);
}
}
class AppWithoutArray extends React.Component {
render() {
return (
<div>
<div> test3 </div>
<div> test4 </div>
</div>
);
}
}
ReactDOM.render(
<div>
<AppWithArray />
<AppWithoutArray />
</div>,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
When a component gets a new key prop, the old one will be unmounted and thrown away and a new one will be created and mounted. You almost never use the key prop outside of arrays, but it can be a nice technique to keep in mind if you ever need to create an entirely new component.
class Timer extends React.Component {
timer = null;
state = { count: 0 };
componentDidMount() {
this.timer = setInterval(() => {
this.setState(prevState => ({ count: prevState.count + 1 }));
}, 1000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
return <div>{this.state.count}</div>;
}
}
class App extends React.Component {
state = { timerKey: 1 };
componentDidMount() {
setTimeout(() => {
this.setState({ timerKey: 2 });
}, 5000);
}
render() {
return <Timer key={this.state.timerKey} />;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>