I tried to search for this and I think the answer is being obfuscated by many questions and tutorials using similar keywords - but not with the specific issue I'm having. This issue is close - where a complex component (D3) is continuously being re-rendered. Lots of answers I find are for memoize and PureComponents - these don't work for d3, Google Maps, etc though - the rendering of Stateful_Class_Component_Widget happens without fail.
I have a React app with a structure similar to the following (very basic sample of general structure)
// big stateful component running everything with state
class AppView extends React.Component {
constructor(props) {
super(props);
this.state = {
documents: [] // array of simple JSON docs fetched from API
show_widget: true
}
this.function1 = this.function1.bind(this)
// more involved functions, set states, etc
render()
return (
<div>
<StatelessFC1
documents = {this.state.documents}
show_widget = {this.state.show_widget}
/>
<StatelessFC2
props={this.state.otherStateThing}
/>
</div>
)
}
class Stateful_Class_Component_Widget extends React.Component {
// assume D3, Google Maps, Leaflet - a big involved
constructor(props) {
super(props);
console.log("New instance")
this.state = {
documents: [] // array of simple JSON docs fetched from API
}
}
// some functions etc
render() {
return (
<div id="d3_uses_me">
// d3/google maps code
</div>
)
}
const StatelessFC1 = (props) =>
return (
<StatelessFC1_a
documents={props.documents}
show_widget = {props.show_widget}
/>
)
const StatelessFC1_a = (props) =>
return (
<StatelessFC1_a_i
documents={props.documents}
show_widget = {props.show_widget}
/>
// this only renders when AppView's state is 'show-widget')
const StatelessFC1_a_i = (props) =>
return (
{ props.show_widget &&
<Stateful_Class_Component_Widget
documents={props.documents}
/>
}
</div>)
So the idea is that in order to structure the application view, I have a function of dumb stateless components that get rerendered as necessary when the AppView state changes - all good there. State for everything else is handled fine by the AppView.
The problem is though - is that the Stateful_Class_Component_Widget (in my case it is a similar to a google map - so re-rendering it each time something trivial changes is pointless) keeps getting re-created - that is each time the state of AppView changes, a new instance of the Stateful_Class_Component_Widget is being created (likely because of the re-rendering triggered in its 'parent' stateless components - which is quite taxing since this is a third party API. Ideally it should only update when its state/props change - which is all good - but right now it is being created everytime AppView's state changes (can see that "new instance" in console each time)
Essentially I want one instance of the Stateful_Class_Component_Widget and want it to remain persistent (though not always visible) throughout the course of the application lifetime. Right now it is always being reconstructed. Is only way to do it to bring it directly into AppView?
Related
I'm currently developing an app that uses React in some parts of its UI to implement some complex feature that are very interactive. Some of these parts of the app, that are in fact React components, have become very complex in size.
This has led me to organize the code by dividing them in multiple subcomponents (and those in their own subcomponents, and so forth). The main problem is that some of the grandchildren of the main component might have the need to modify the state of another grandchildren that they're not related to. To do so, I end up having to have the state in the common parent and passing a lot of event handlers (methods of the parent component) to their children, and those children would need to pass it to their grandchildren.
As you can imagine, this is becoming some kind of a mess.
// MyComponent.js
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [1, 2, 3, 4],
selected: '',
}
this.add = this.add.bind(this)
this.handleChange = this.handleChange.bind(this)
}
add() {
const newNumber = this.state.list[this.state.list.length - 1] + 1,
list = [...this.state.list, newNumber]
this.setState({list})
}
handleChange({target}) {
this.setState({
selected: target.value,
})
}
render() {
return (
<>
<List items={this.state.list} selected={this.state.selected} />
<Button onClick={this.add} />
<input type="text" value={this.state.selected} onChange={this.handleChange} />
</>
)
}
}
// Button.js
class Button extends React.Component {
render() {
return (
<button onClick={this.props.onClick}>Click me!</button>
);
}
}
// List.js
class List extends React.Component {
constructor(props) {
super(props)
this.refs = props.items.map(_ => React.createRef())
}
render() {
return (
<ul>
{this.props.items.map((item, key) =>
(<li ref={this.ref[key]} key={key}>{item}</li>)
)}
</ul>
);
}
}
In the previous dummy code you can see how I need to define the add() method in the MyCompoent component so that an action that happens in the Button component can modify what is being shown in List. Even tho this might seem like the obvious way to do it, my component has a big component tree, and a lot of methods, and most of then are lost in the tree, passing from parent to child until it reaches the component that should be expected.
I have done some research on the internet and it turns out this is a very common problem. In most sites, using Redux or other state management library is recommended. However, all the tutorials and guides I've seen that implement Redux with React seem to assume you're only using React to build your app, in Single Page Application sort of way. This is not my case.
Is there any way to share the state of a component to avoid this kind of problem? Is there, maybe, a way to use Redux multiple times for multiple components in the same app, where one store saves only the state for MyComponent and can be accessed by either List or any of its possible children?
Redux doesn't require your entire site to be in React. It implements a higher-level component that you can use with any React components even if they are embedded in another site.
You can look at React hooks to solve similar problems. Specifically, check out useContext() and useState().
You've used a lifting state up pattern in react in your example.
It's quite common you good approach but when you app is growing you need to pass all bunch of props throu the tree of components. It's difficult to maintain.
In this case you need to check out redux with separated store or useContext() hook.
I have CompetitionSection which repeats all the competitions from database. When user clicks on one, it redirects him to a Competition Page, loads for a second and renders the page with all the details in it. So far, so good.
But when users goes back to the Competition Section and then click on the second competition, it instantly loads up the previous competition, 0 loading time.
From my point of view, what is failing is that the props of the component are not updating when I render the component (from the second time). Is not a router problem, which was my first instinct because I'm seeing the route.params changing acordingly, but the actions I dispatch to change the props are not dispatching. Here's a bit of code of said component.
class CompetitionPage extends React.Component {
componentWillMount() {
let id = getIdByName(this.props.params.shortname)
this.props.dispatch(getCompAction(id));
this.props.dispatch(getCompMatches(id));
this.props.dispatch(getCompParticipants(id));
this.props.dispatch(getCompBracket(id));
}
render() {
let { comp, compMatches, compBracket, compParticipants } = this.props
...
I tried every lifecycle method I know. component Will/Did Mount, component Will/Did update and I even set shouldUpdate to true and didn't do the trick. As I understand, the problem will be solved with a lifecycle method to dispatch the actions everytime an user enters Competition Page and not just for the first time. I'm running out of options here, so any help will be appreciated.
NOTE: I'm a newbie at React/Redux so I KNOW there are a couple of things there are anti-pattern/poorly done.
UPDATE: Added CompetitionsSection
class CompetitionsSection extends React.Component {
render() {
const {competitions} = this.props;
return (
...
{ Object.keys(competitions).map(function(comp, i) {
return (
<div key={i} className={competitions[comp].status ===
undefined? 'hide-it':'col-xs-12 col-md-6'}>
...
<Link to={"/competitions/"+competitions[comp].shortName}>
<RaisedButton label="Ver Torneo" primary={true} />
</Link>
...
It helps to better understand the lifecycle hooks. Mounting a component is when it is placed on the DOM. That can only happen once until it is removed from the DOM. An UPDATE occurs when new props are passed or setState is called. There are a few methods to troubleshoot when updates are not happening when you think they should:
Ensure that you are changing state in componentDidMount or componentDidUpdate. You cannot trigger an update in componentWillMount.
Make sure that the new props or state are completely new objects. If you are passing an object down in props and you are just mutating the object, it will not trigger an update. For instance, this would not trigger a update:
class CompetitionPage extends React.Component {
constructor(props) {
super(props)
this.state = {
competitions: [ compA, compB ]
}
}
triggerUpdate() {
this.setState({
competitions: competitions.push(compC)
})
}
componentDidMount() {
triggerUpdate()
}
render() {
return(
<div>
Hello
</div>
)
}
This is due to the fact that a new competition is being appended to the array in state. The correct way is to completly create a new state object and change what needs to be changed:
const newCompetitions = this.state.competitions.concat(compC)
this.setState(Object.assign({}, this.state, { competitions: newCompetitions }))
Use ComponentWillRecieveProps on an update to compare previous and current prop values. You can setState here if clean up needs to be done:
Read more about this method in the React documentation:
https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops
I have a feeling this is a "wrong" question to ask, but here goes anyway:
I'm making some sort of quiz app (using redux for state management). (showing the important bits here)
quiz.js
<Slider {...sliderSettings} slideIndex={currentQuestionIndex}>
<Start onStart={() => onNextQuestion()} topicId={topicId} />
{
questions.map((question, ndx) => {
return (
<Question {...question} done={onDone} key={`question-${question.id}`} />
);
})
}
<Result score={score} onRestart={() => onRestart()}/>
</Slider>
question.js
<div className="question">
<h2 className="question__text">{ question }</h2>
<MultipleChoice options={answers} onChange={done} />
</div>
multiple-choice.js
const
initialState = {
selectedValue: null
};
class MultipleChoice extends Component {
constructor(props) {
super(props);
this.state = initialState;
}
handleChange(value, correct) {
this.setState({
selectedValue: value
});
this.props.onChange(correct);
}
render() {
const
{ options } = this.props,
getStateClass = (option, ndx) => {
let sc = '';
if (this.state.selectedValue !== null) {
if (this.state.selectedValue === ndx) {
sc = option.correct ? 'is-correct' : 'is-incorrect';
} else if (option.correct) {
sc = 'is-correct';
}
}
return sc;
};
return (
<ul className="multiple-choice">
{ options.map((option, ndx) => {
return (
<li key={`option-${ndx}`} className={cx('multiple-choice__option', getStateClass(option, ndx))}>
<button className="multiple-choice__button" onClick={() => this.handleChange(ndx, option.correct)}>{option.answer}</button>
</li>
);
}) }
</ul>
);
};
}
export default MultipleChoice;
The problem lies within the rendering of MultipleChoice. It uses internal state to show which answer is wrong and right.
in quiz.js, onRestart dispatches a redux action which updates the store to fetch some new questions and reset the currentQuestionIndex to 0. This all works.
But somehow, sometimes the MultipleChoice element is "reused" and is still showing the state it had in the previous round of questions. In other words, most of the time a new MultipleChoice gets mounted, but sometimes it isn't. This is react reconciliation, if I understand correctly?
But how do I solve this problem? In my view, MultipleChoice needs its internal state. So should I reset this state somehow? Or make sure a new MultipleChoice gets mounted everytime? Or am I asking the wrong questions here?
I looked at your repository, and the issue, as correctly noted in another answer is that your <Question> (and thus inner <MultipleChoice>) components never unmount, so they keep their state.
Normally this doesn’t come up often in React because people usually want the state to be preserved while the component is in the tree. When the state is no longer needed, people usually stop including components in the render() method, and React unmounts them. Next time they are rendered, their state gets reset.
The state does not get reset in your example because you always keep the <Question>s visible, even between the quiz runs. You can see that <Question>s are already mounted before we begin the quiz, and stay mounted after it ends:
So how can we force React to reset their state? There are three options:
You may cause them to unmount. Next time they get mounted, they’ll have a new state. This is usually the simplest solution, because you don’t actually display the questions on the initial “start quiz” page. For example, you can do this by adding currentQuestionIndex > 0 && guard before rendering questions.map(...) in the render() method of <Questions>.
You may pass new keys to them that don’t match previous keys. You are currently using question-${question.id} as the key right now but that will produce the same key for the same question even if you retry the quiz. To solve this, you could introduce a new state variable (either in Redux or in top-level component state), for example, quizAttemptIndex, and increment it on any new attempt. Then you could change the key to be question-${quizAttemptIndex}-${question.id}. This way attempting a quiz another time would reset the internal state of the question (as well as cause it to remount).
Finally, if you’d rather not destroy the DOM completely by passing a different key, you could pass quizAttemptIndex (explained in the previous section) as a prop to <MultipleChoice>. Then, inside it, you could this.setState({ selectedValue: null }) inside componentWillReceiveProps(nextProps) if nextProps.quizAttemptIndex !== this.props.quizAttemptIndex.
You can choose either solution depending on how important it is for you to keep the questions mounted all the time.
As far as I understood, you want your MultipleChoice component to refresh its state when you want it to do so. But as you are using react state in your component, as long as your component doesn't unmount, your react state in your MultipleChoice keeps its latest state.
This behaviour is expected from react state, because mostly you want to use it for internal behaviour. Maybe you want to toggle some ui data in your component, when some button or something triggers your component to do so. Or you want to control your input forms etc.
But what you expect should be accomplished in redux state. Your component should be reusable which doesn't care where it mounted. You pass your mounted-place-related data to your MultipleChoice with props, which is taken from redux. So now you have a reusable component. Don't spend much time about mounting etc. You might check react-redux repositories on github to get familiar with how to shape your project's data flow. When to use redux state to make desicions or handle state in your component by react state.
I am using same component multiple times in the same page, and I just realized that any event dispatched are intercepted by all the same companents and all the components are updated together.
This is not acceptable, as even if it is same component, if it is used to display different data, they should have totally independent behavior. Action performed in one component should never be listened by another component.
How can I fix this error?
You should have a container component which will get a data collection, which represents the component you are repeating. An action will change that data collection, and not the repeated component itself. In other words, the repeated component should not get data directly from the store.
You could see an full todomvc example, which has the same "TodoItem" component being rendered a few times in one page here: TodoMVC example
Example:
var ButtonStore = require('../stores/ButtonStore');
function getButtonState() {
return {
allButtons: ButtonStore.getAll()
}
}
const Button = (props) => {
return <button>{props.text}</button>
}
class ButtonList extends React.Component {
constructor(props) {
super(props)
this.state = getButtonState()
}
render() {
return <div>
{this.state.allButtons.map(button => <Button {...button} />)}
</div>
}
}
Here is a fiddle of the example, just without the store: Component List Example
It could be helpful, if you post some code example.
Imagine you have a relatively simple component you create as part of a component library (simplified for brevity):
class ExampleComponent extends React.Component {
componentDidMount() {
getAsyncData().then((response) => {
const {a} = response.data;
this.setState({a});
this.props.notify({a});
});
}
render() {
return (
<h1>{this.state.a}</h1>
);
}
}
The component is required to allow dropping it into an application (think Google Maps for relatively similar approach) and have it just work. It can, however, share its data from response with the rest of the application, via some sort of callback (see this.props.notify above) it may receive via its props. This is an actual requirement and not a architectural choice.
Since this is a part of a library - you don't know what kind of application it is going to get used in at all times, but you do know that in many many cases it is going to get used in a Redux application.
For Redux application the above self-contained approach is not necessarily the best - as the retrieved data in response is better kept in application state in Redux store, where it can be consumed by other parts of application.
Even more so - the ExampleComponent itself is better off being "passive" and not having state at all, rather using mapStateToProps to have Redux infrastructure inject the state update into it via props.
The idea is that when ExampleComponent is in Redux application - its setState call and reference to this.state in its render method are somehow abstracted and "re-routed" to props via Redux?
One way would be to make ExampleComponent to use dispatch that by default calls setState and can be overridden by injected Redux dispatch - basically take this to Redux:
class ExampleComponent extends React.Component {
constructor() {
super();
this.dispatch = this.props.dispatch || this.dispatch;
}
componentDidMount() {
getAsyncData().then((response) => {
this.dispatch({type: 'SOME_ACTION', data: response.data});
});
}
dispatch(action) {
swtich (action.type) {
case 'SOME_ACTION':
const {a} = action.data;
this.setState({a});
case 'ANOTHER_ACTION': ...
}
}
render() {
return (
<h1>{this.state.a}</h1>
);
}
}
The above example works very well, save for:
this.state.a and its kin being sprinkled around the code whereas in Redux it should be this.props.state
having to do this.dispatch = this.props.dispatch || this.dispatch; in every component
I would like to avoid the obvious BaseComponent solutions that would abstract setState into some kind of hybrid... as this would take the code, with time, further away from "canonical" React.
Do you see an elegant way where the two approaches can be combined, with Redux superseding the inherent one?
You're making a fundamental mistake in thinking that a React component with Redux is different from a React component without Redux.
In fact, a React component is just a React component.
This is all your component needs to look like:
function ExampleComponent({ a }) {
return (
<h1>{a}</h1>
);
}
Simple, clean, readable, testable.
There's no obvious reason why your asynchronous data fetch should be buried inside the component's componentDidMount() method. It can be triggered anywhere else in the application. And it should be.