wait for API call, then pass on initial value to component - reactjs

I am having difficulties with creating an initial state for a component which relies on an API call.
In every way I tried to implement it, it get's into a loop of giving me the error that this.setState() is called during rendering, which is an anti patern,...
Question:
How is it possible to implement an initial state for a parent component depending on a child component's state? Only firing if there is nothing in the reduxstate
I could show code but it's quite extensive and divided between multiple files.
EDIT 1: As requested - simplified code
Parent component (Filter)
class Filter extends Component {
constructor(props) {
super(props);
this.state = {
active: '',
period: this.props.filter.period,
periodDisplay: null,
departments: this.props.filter.departments,
departmentsDisplay: null
};
this.updateFilterDepartments = this.updateFilterDepartments.bind(this);
this.updateFilterPeriod = this.updateFilterPeriod.bind(this);
this.initDepartments = this.initDepartments.bind(this);
}
componentDidMount() {
// Getting initial period
if (!this.state.period && this.props.initialFrom && this.props.initialTo) {
this.didSelectPeriod(this.props.initialFrom, this.props.initialTo);
}
else if (this.state.period) {
let initPeriod = this.state.period.match(/.{1,2}/g);
this.didSelectPeriod(
initPeriod[2] + initPeriod[3] + '-' + initPeriod[1] + '-' + initPeriod[0],
initPeriod[6] + initPeriod[7] + '-' + initPeriod[5] + '-' + initPeriod[4]
);
}
}
componentWillUnmount(){
this.setState({active: ''});
}
updateFilterPeriod() {
this.props.updateFilterPeriod(this.state.period);
}
didSelectPeriod(rawFrom, rawTo) {
if (rawFrom && rawTo) {
let From = moment(rawFrom),
To = moment(rawTo);
if (From.diff(To, 'days') === 0) {
this.setState ({
periodDisplay: From.format('D MMM YYYY'),
period: From.format('DDMMYYYY') + To.format('DDMMYYYY')
}, () => this.updateFilterPeriod());
}
else {
this.setState ({
periodDisplay: From.format('D MMM YYYY') + ' - ' + To.format('D MMM YYYY'),
period: From.format('DDMMYYYY') + To.format('DDMMYYYY')
}, () => this.updateFilterPeriod());
}
}
else {
this.setState({
periodDisplay: null,
period: null
}, () => this.updateFilterPeriod());
}
}
updateFilterDepartments() {
this.props.updateFilterDepartments(this.state.departments);
}
didSelectDepartments(hashes) {
if (hashes.length === 0) {
this.setState({
departments: null,
departmentsDisplay: null
}, () => this.updateFilterDepartments());
}
else {
this.setState({
departments: hashes,
departmentsDisplay: hashes.length + ' department(s) selected'
}, () => this.updateFilterDepartments());
}
}
render() {
return (
<div className='FILTER_wrapper' >
<div className={classnames('FILTER_section', {active: this.state.active == 'datepicker'})} onClick={() => this.setState({active: 'datepicker'})}>
<i className="fa fa-calendar FILTER_icon FILTER_icon--datepicker"></i><div className="FILTER_input FILTER_input--datepicker">{this.state.periodDisplay || <div className="placeholder">Select a period</div>}</div>
<div className={classnames('FILTER_dropdown FILTER_dropdown--datepicker', {active: this.state.active == 'datepicker'})}>
<DatePicker className='' didSelect={(rawFrom, rawTo) => this.didSelectPeriod(rawFrom, rawTo)} />
</div>
</div>
<div className={classnames('FILTER_section', {active: this.state.active == 'departmentpicker'})} onClick={() => this.setState({active: 'departmentpicker'})}>
<i className="fa fa-tags FILTER_icon FILTER_icon--departmentpicker"></i><div className="FILTER_input FILTER_input--departmentpicker">{this.state.departments ? this.state.departmentsDisplay : <div className="placeholder">Select department(s)</div>}</div>
<div className={classnames('FILTER_dropdown FILTER_dropdown--departmentpicker', {active: this.state.active == 'departmentpicker'})}>
<DepartmentPicker didSelect={(hashes, names) => this.didSelectDepartments(hashes)} />
</div>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
departments: state.departments,
filter: state.filter
};
}
Child component (DepartmentFilter)
class DepartmentPicker extends Component {
callCallback() {
this.props.didSelect(this.state.selectedHash, this.state.selectedName);
}
handlePredefined(type) {
let newStateHash = [];
let newStateName = [];
switch(type) {
case 'all':
this.props.departments.items.map(department => {
newStateHash.push(department.hash);
newStateName.push(department.name);
});
this.setState({
selectedHash: newStateHash,
selectedName: newStateName
}, () => this.callCallback());
break;
case 'none':
this.setState({
selectedHash: newStateHash,
selectedName: newStateName
}, () => this.callCallback());
break;
}
}
handleDepartment(hash, name) {
if (this.state.selectedHash.length > 0 && this.state.selectedHash.indexOf(hash) > -1) {
let newStateHash = this.state.selectedHash;
let newStateName = this.state.selectedName;
let index = newStateHash.indexOf(hash);
newStateHash.splice(index, 1);
this.setState({
selectedHash: newStateHash,
selectedName: newStateName
}, () => this.callCallback());
}
else {
this.setState({
selectedHash: [...this.state.selectedHash, hash],
selectedName: [...this.state.selectedName, name]
}, () => this.callCallback());
}
}
renderDepartments(department) {
let inSelection = this.state.selectedHash.length > 0 && this.state.selectedHash.indexOf(department.hash) > -1 ? true : false;
return(
<li className={classnames('DEPARTMENTPICKER_department', {active: inSelection})} key={department.hash} onClick={() => this.handleDepartment(department.hash, department.name)}>
<i className={classnames('DEPARTMENTPICKER_icon', 'fa', {'fa-check': inSelection, 'fa-times': !inSelection})} style={{color: department.theme}}></i>
{department.name}
</li>
);
}
render() {
// Notice that here it waits for the end of the api call
if (this.props.departments.isFetching) {return <Loader type='spinner' />;}
return (
<div className='DEPARTMENTPICKER_wrapper'>
<ul className='DEPARTMENTPICKER_predefined'>
<li onClick={() => this.handlePredefined('all')}>All</li>
<li onClick={() => this.handlePredefined('none')}>None</li>
</ul>
<ul className="DEPARTMENTPICKER_departments">
{this.props.departments.items.map(this.renderDepartments)}
</ul>
</div>
);
}
}
function mapStateToProps(state) {
return {
departments: state.departments
};
}
export default connect(mapStateToProps)(DepartmentPicker);
EDIT 2
Because some people are asking about the flow the application is following I decided to include it here:
Parent component renders + API call (triggered by loggin in)
Child component renders (API call still in progress)
API call finishes -> need to create initial state for child component

componentWillMount will fire only one time so setting state over there is not a good idea. But if you want to set state for one time then you can use componentWillMount. otherwise there are three options:
According to the sequence of fire:
1)componentWillReceiveProps, Which will be invoked when props are changed
2)shouldComponentUpdate, this lifecycle function will give permission to render or not by returning true or false.
3)ComponentWillUpdate, this will be fired just before render is called. you can compare new props with old props and setState.

I've had a quick look. Seems you're updating the state in componentDidMount (via didSelectPeriod) in the parent. That's guaranteed unnecessary render. You should be moving that code to
constructor if it is a one time init thing
componentWillReceiveProps to if you want it to be executed when a relevant prop changes.
Now, this way you may or not get rid of the error mesage but there are more apparent issues with your code:
You're trying to render a redux container in a redux container. Well, this is technically doable but very wrong from an architectural perspective and probably the root of your problem. Try to imagine how simple your code would be if you just passed departments to your child components instead of using a connected component. Please refer to smart / dumb components in Redux manual.
componentWillUnmount(){
this.setState({active: ''});
}
This is not needed as a component's state is deleted as it is unmounted.
didSelect={(rawFrom, rawTo) => this.didSelectPeriod(rawFrom, rawTo)} Don't create a function every time you render. Use: didSelect={this.didSelectPeriod} instead. Don't forget to bind in the constructor: this.didSelectPeriod = this.didSelectPeriod.bind(this);
Using redux middleware (thunk, promise) would greatly simplify your API calls.
Try to put more context to your components. Is it a layout, a container or a dumb component? Most of the the time these three are all you need and layout > container > component is your usual render order.

Related

Trouble understanding React stale state in Counter example

I'm sure my issues is around useCallback and the setter method I have here setSpeakers returned from useState but I'm not understanding exactly why my example, as listed here and in codesandbox is not incrementing the state correctly. The old state keeps being returned, not the new incremented state. When I pass a function into setSpeakers instead of just a new state, my example works (you can see the commented out code marked THIS CODE WORKS below the non-working code.
I know others have written articles on this and I've read those articles but still don't get it.
sandbox: https://codesandbox.io/s/elated-lichterman-7rejs?file=/pages/index.js
import React, { useState, memo, useCallback } from "react";
//const Speaker = React.memo(({ imageSrc, counter, setCounter }) => {
const Speaker = ({ speaker, speakerClick }) => {
console.log(speaker.id);
return (
<div>
<span
onClick={() => {
speakerClick(speaker.id);
}}
src={`speakerimages/Speaker-${speaker.id}.jpg`}
width={100}
>
{speaker.id} {speaker.name}
</span>
<span className="fa fa-star "> {speaker.clickCount}</span>
</div>
);
};
function SpeakerList({ speakers, setSpeakers }) {
return (
<div>
{speakers.map((speaker) => {
return (
<Speaker
speaker={speaker}
speakerClick={useCallback((id) => {
// THIS CODE FAILS BECAUSE OF STALE STATE BUT I DON'T GET WHY
const speakersNew = speakers.map((speaker) => {
return speaker.id === id
? { ...speaker, clickCount: speaker.clickCount + 1 }
: speaker;
});
setSpeakers(speakersNew);
// THIS CODE WORKS
// setSpeakers(function(speakers) {
// const speakersNew = speakers.map((speaker) => {
// return speaker.id === id
// ? { ...speaker, clickCount: speaker.clickCount + 1 }
// : speaker;
// });
// return speakersNew;
// });
}, [])}
key={speaker.id}
/>
);
})}
</div>
);
}
//
const App = () => {
const speakersArray = [
{ id: 1124, name: "aaa", clickCount: 0 },
{ id: 1530, name: "bbb", clickCount: 0 },
{ id: 10803, name: "ccc", clickCount: 0 }
];
const [speakers, setSpeakers] = useState(speakersArray);
return (
<div>
<h1>Speaker List</h1>
<SpeakerList speakers={speakers} setSpeakers={setSpeakers}></SpeakerList>
</div>
);
};
export default App;
That's because set state works asynchronously. In the first code that isn't running you set the state and try to immediately have it show up which wont work since there is delay while the state is being set. The second one works because you're passing a callback to set state which basically means set the state and do this while you're at it. while the first one means set the state and then do this afterwards. That's not what you want. Its a loose analogy but look into how and why set state is aync you'll have a better idea. Cheers.

Having trouble making a prop update dynamically in countdown timer - create with a react hook

I'm new to React and building a project that requires dynamically setting a countdown timer from a parent component. I found a react countdown timer online that uses a hook, but I'm not too familiar hooks yet.
Toward the bottom of my code, you can see a parent class where I'm passing 'cycleTimeSelected' to the Countdown component/hook. It works correctly up to that point.
But I'm not having success getting it to update the timer correctly and dynamically. timeRemaining in React.useState() is the variable I need to update. It doesn't work if I use props.cycleTimeSelected directly. I think I understand why that is, so I tried to use componentWillRecieveProps to set the state, but that's not working.
I'm think I'm confused about React.useState and how that relates to setState for one thing. Can anyone spot my problem here?
import React, { Component } from 'react'
import { PausePresentation, SkipNext, Stop, PlayCircleOutline} from '#material-ui/icons';
import './Timer.css'
function Countdown(props) {
const [timer, setTimer] = React.useState({
name: 'timer',
isPaused: true,
time: 100,
timeRemaining: props.cycleTimeSelected,
timerHandler: null
})
//const componentWillReceiveProps = nextProps => {
//this.setState({ timeRemaining: nextProps.cycleTimeSelected });
//}
const handleTimeChange = e => {
setTimer({
...timer,
time: props.cycleTimeSelected,
timeRemaining: Number(e.target.value),
})
}
React.useEffect(() => {
if (timer.timeRemaining === 0) {
clearInterval(timer.timerHandler)
}
}, [timer.timeRemaining, timer.timerHandler])
const updateTimeRemaining = e => {
setTimer(prev => {
return { ...prev, timeRemaining: prev.timeRemaining - 1 }
})
}
const handleStart = e => {
const handle = setInterval(updateTimeRemaining, 1000);
setTimer({ ...timer, isPaused: false, timerHandler: handle })
}
const handlePause = e => {
clearInterval(timer.timerHandler)
setTimer({ ...timer, isPaused: true })
}
return <React.Fragment>
{/* { <input value={props.cycleTimeSelected} type="" onChange={handleTimeChange} /> } */}
{timer.name && timer.time && <div className={timer.timeRemaining === 0 ? 'time-out':''}>
{timer.isPaused && <div className="floatLeft"><i className="material-icons pause"><PlayCircleOutline onClick={handleStart}></PlayCircleOutline></i></div>}
{!timer.isPaused && <div className="floatLeft"><i className="material-icons pause"><PausePresentation onClick={handlePause}></PausePresentation></i></div>}
{`Remaining: ${timer.timeRemaining}`}
</div>}
</React.Fragment>
}
class Timer extends React.Component {
render(){
return(
<>
<div className="floatLeft"><i className="material-icons bottom-toolbar stop"><Stop onClick={this.clickStop}></Stop></i></div>
<div className="floatLeft"><i className="material-icons bottom-toolbar skip_next"><SkipNext onClick={this.clickSkip}></SkipNext></i></div>
<div className="floatLeft"><div id="timer"><Countdown cycleTimeSelected={this.props.cycleTimeSelected}></Countdown></div></div>
</>
);
}
}
If you want timer.timeRemaining in Countdown to update via props then you should implement an effect with a dependency on props.cycleTimeSelected. useEffect is nearly the functional component equivalent to a class-based component's componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle functions. We're interested in the "update" lifecycle.
useEffect(() => {
setTimer((timer) => ({
...timer, // copy existing state
timeRemaining: props.cycleTimeSelected // update property
}));
}, [props.cycleTimeSelected]);

Creating custom pagination, but next and prev buttons are not working

I'm trying to create my own pagination (without using a package), but I can't get it to work.
I'm wondering if it has something to do with how I'm copying my arrays, but I'm not really sure.
class InsightSearchResults extends Component {
state = {
start: 0,
end: 2,
insightsArrayOriginal: [],
copiedArr: []
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.insightList[0]) {
this.setState({
insightsArrayOriginal: nextProps.insightList[0].insights,
copiedArr: nextProps.insightList[0].insights.splice(this.state.start, this.state.end)
})
}
}
clickNext = () => {
let copied = [...this.state.insightsArrayOriginal];
this.setState({
start: this.state.start + 2,
end: this.state.end + 2
}, () => {
this.setState({
copiedArr: copied.splice(this.state.start, this.state.end)
})
})
}
clickPrev = () => {
this.setState({
start: this.state.start - 2 < 0 ? 0 : this.state.start - 2,
end: this.state.end - 2
})
}
render() {
const { copiedArr } = this.state;
return (
<div style={{padding: "1.5rem"}}>
{copiedArr ? copiedArr.map(insight => (
<div>
<Grid className="insight_result_block">
<Col className="insight_results_col2" span="10">
<div>
<h4>Hello</h4>
<p>{insight.insightDesc}</p>
</div>
</Col>
</Grid>
<hr className="bottom_hr_insight" />
</div>
)) : <p>loading...</p> }
<button onClick={this.clickPrev}>Prev</button>
<button onClick={this.clickNext}>Next</button>
</div>
)
}
}
I haven't really worked on the "prev" part yet. I'm just trying to get the "next" to work for now...
There are two problems:
UNSAFE_componentWillReceiveProps is not called on initial render. From the docs:
React doesn’t call UNSAFE_componentWillReceiveProps() with initial
props during mounting. It only calls this method if some of
component’s props may update. Calling this.setState() generally
doesn’t trigger UNSAFE_componentWillReceiveProps().
splice mutates the original array, use slice instead. See this question.
So you can move the content of UNSAFE_componentWillReceiveProps to componentDidMount and componentDidUpdate
componentDidMount() {
this.updateState();
}
componentDidUpdate() {
// check if a change in props has caused the rerender
// or you will get infinite rerenders if one state update causes the next one
if (
this.props.insightList[0] &&
this.props.insightList[0].insights !== this.state.insightsArrayOriginal
) {
this.updateState();
}
}
These functions don't receive a parameter: replace nextProps parameter with this.props; and change all splice occurrences with slice.
updateState() {
if (this.props.insightList[0]) {
this.setState({
insightsArrayOriginal: this.props.insightList[0].insights,
copiedArr: this.props.insightList[0].insights.slice( . // <-here
this.state.start,
this.state.end
)
});
}
}
clickNext = () => {
let copied = [...this.state.insightsArrayOriginal];
this.setState({ start: this.state.start + 2, end: this.state.end + 2 },
() => {
this.setState({
copiedArr: copied.slice(this.state.start, this.state.end) // <- and here
});
}
);
};
Also, based on this code sample alone, you could entirely remove insightsArrayOriginal from your state and use it from props, but this may change if you plan to expand the functionality.

Reactjs show hide multiple components

Newbie React question here on show hide functionality.
I have a state of 'show' that I set to false:
this.state = {
show: false,
};
Then I use the following function to toggle
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
And my display is
{this.state.show && <xxxxxx> }
This all works fine. However I want to apply the function it to multiple cases (similar to accordion, without the closing of other children. So I change my constructor to
this.state = {
show: [false,false,false,false,false,false]
};
and this to recognise there are 6 different 'shows'.
{this.state.show[0] && <xxxxxx> }
{this.state.show[1] && <xxxxxx> } etc
But where I get stuck is how to account for them in my toggleDiv function. How do I insert the square bracket reference to the index of show (if this is my problem)?
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
Thanks for looking.
First of all I'd suggest you not to rely on current state in setState function, but to use the callback option to be 100% sure that you are addressing to the newest state:
this.setState((prevState) => ({ show: !prevState.show }));
How to deal with multiple elements?
You'll have to pass the index of currently clicked element.
{yourElements.map((elem, i) => <YourElem onClick={this.toggleDiv(i)} />)}
and then inside your toggleDiv function:
toggleDiv = (i) => () => {
this.setState((prevState) => {
const r = [...prevState.show]; // create a copy to avoid state mutation
r[i] = !prevState.show[i];
return {
show: r,
}
}
}
Use an array instead of a single value. In your toggle div function make a copy of the state array make necessary changes and push the entire array back up to state at the end.
This is some simplified code showing the workflow I described above
export default class myClass extends React.Component{
constructor(props){
super(props);
this.state = { show: new Array(2).fill(false) };
}
//you need a index or id to use this method
toggleDiv = (index) => {
var clone = Object.assign( {}, this.state.show ); //ES6 Clones Object
switch(clone[index]){
case false:
clone[index] = true
break;
case true:
clone[index] = false
break;
}
this.setState({ show: clone });
}
render(){
return(
<div>
{ this.state.show[0] && <div> First Div </div> }
{ this.state.show[1] && <div> Second Div </div> }
{ this.state.show[2] && <div> Third Div </div> }
</div>
)
}
}

Error: findComponentRoot when using divs

I am using react and redux but I'm getting the following Exception:
foo.js:10 Uncaught Error: findComponentRoot(..., .0.$data.0.0.$22): Unable to find element. This probably means the DOM was unexpectedly mutated (e.g., by the browser), usually due to forgetting a when using tables, nesting tags like <form>, <p>, or <a>, or using non-SVG elements in an parent. Try inspecting the child nodes of the element with React ID
when I press on the button below:
let mapStateToProps = (state) => {
const user = state.user.get("user");
return {
data: state.data.get("data"),
loading: state.pausedAlarms.get("loading"),
userId: user&&user.id
}
};
let mapActionsToProps = (dispatch) => {
return {
getData() {
dispatch(getData());
},
selectUser(user){
dispatch(selectUser(user));
},
};
};
let Foo = React.createClass({
componentWillMount() {
this.props["getData"]();
},
shouldComponentUpdate(nextProps, nextState) {
if(nextProps["userId"] !== this.props["userId"]) {
nextProps["getData"]();
}
return true;
},
render() {
const {
data,
selectUser,
loading
} = this.props;
if (loading) {
return (
<div id="3" key="3">LOADING</div>
);
}
else if (data){
return (
<div id="1" key="1">
<div id="11" key="11">
HEADER
</div>
<button
id="2"
key="22"
onClick={
e => {
selectUser(new User({id:1,name:"Foo"}))
}
}
>Click</button>
</div>
);
}
else{
return null;
}
}
});
Pressing the button dispatches the action selectUser, which updates my redux state with a new value for userId. This causes shouldComponentUpdate to call nextProps["getData"]();. The action getData begins by dispatching such that loading is set to true in the reducer.
The code above renders as expected its just that I'm seeing an exception in the console window.
If I do the following instead:
shouldComponentUpdate(nextProps, nextState) {
if(nextProps["userId"] !== this.props["userId"]) {
nextProps["getData"]();
return false;
}
return true;
},
my code works as expected but without the exception.
What am I doing wrong? How might I debug this problem?
I guess the problem is caused that you are dispatching an action in shouldComponentUpdate.
You can dispatch it in componentWillReceiveProps:
componentWillReceiveProps(nextProps) {
if(nextProps["userId"] !== this.props["userId"]) {
nextProps["getData"]();
}
}
shouldComponentUpdate is intended to determine whether a component should be updated, instead of updating the state. Take a look at the following SO discussion:
Is it OK to call setState from within shouldComponentUpdate?
Update 1
Check if the problem is caused by the render structure. Try to simplify it (remove keys, mouniting/unmounting nodes and so on):
getData() {
return (
<div>
HEADER
<button onClick={ e => { selectUser(new User({id:1,name:"Foo"})) } }>Click</button>
</div>
);
}
render() {
const {
data,
selectUser,
loading
} = this.props;
return (
<div>
{ loading ? 'LOADING' : null }
{ (! loading) && data ? this.getData() : null }
</div>
);
}

Resources