React- How to update child prop based on parent state - reactjs

I'm running into a problem getting a child react component to update when its parent stage changes. I have an Editor parent component that sets its state and then updates the state if the component receives an updated schedule (from a graphQL mutation component).
The problem is that componentDidUpdate triggers which does trigger the Modefield to update, but it is before the setState in componentDidUpdate can update the state. This means the child doesn't update. (Note- I know a more idiomatic way is to get rid of state all together, but this way allows a field to both edit and create a new one.)
How can I cause the child to update based on the parent's state change?
export const updateScheduleMutation = gql`
mutation updateScheduleMutation(
$id: ID!
$mode: String
) {
updateSchedule(
id: $id
mode: $mode
) {
id
mode
}
}
`;
class EditorWrapper extends React.Component {
constructor(props) {
super(props);
this.state = { scheduleId: props.scheduleId || '' };
}
render() {
return (
<Mutation mutation={updateScheduleMutation}>
{(updateSchedule, { mutationData }) => <Editor {...data} updateSchedule={updateSchedule} />}
</Mutation>
)
}
}
class Editor extends React.Component {
constructor(props) {
super(props);
const { schedule } = props;
if(schedule === null){
this.state = {
schedule: { mode: schedule.mode || "" }
};
}
}
componentDidUpdate(prevProps) {
if (prevProps.schedule !== this.props.schedule) {
this.setState({ ...this.props.schedule });
}
}
changeInput = (path, input) => {
const { updateSchedule, schedule } = this.props;
const field = path.split('.')[1];
updateSchedule({ variables: { id: schedule.id, [field]: input } });
this.setState({ [path]: input });
};
render() {
return (
<ModeField input={this.state.schedule.input} />
);
}
}
const ModeField = ({input}) => FormControl value={input} />
EDIT: I updated the component to show the higher level graphQL wrapper. The reason why I wanted state in the Editor component is that in the event the graphQL query comes back as null, I set this.state.mode to an empty string, which I then update on change. Then, I would create a new schedule item on submit.

LIFT THE STATE UP! Try to manage the base state of your data in parent component and use the data as props in your component:
You also can try getDerivedStateFromProps, but before check the react blog advices:
https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html

Related

How do I limit the user to only selecting one component?

I have the following code that simply constructs blocks for our products and the selected state allows the component to be selected and unselected. How can I figure out which of these components are selected and limit the user to only selecting one at a time. This is ReactJS code
import React from 'react';
export default class singleTile extends React.Component{
constructor(props){
super(props);
this.title = this.props.title;
this.desc = this.props.desc;
this.svg = this.props.svg;
this.id = this.props.id;
this.state = {
selected: false
}
}
selectIndustry = (event) => {
console.log(event.currentTarget.id);
if(this.state.selected === false){
this.setState({
selected:true
})
}
else{
this.setState({
selected:false
})
}
}
render(){
return(
<div id={this.id} onClick={this.selectIndustry}className={this.state.selected ? 'activated': ''}>
<div className="icon-container" >
<div>
{/*?xml version="1.0" encoding="UTF-8"?*/}
{ this.props.svg }
</div>
</div>
<div className="text-container">
<h2>{this.title}</h2>
<span>{this.desc}</span>
</div>
</div>
)
}
}
You need to manage the state of the SingleTile components in the parent component. What i would do is pass two props to the SingleTile components. A onClick prop which accepts a function and a isSelected prop that accepts a boolean. Your parent component would look something like this.
IndustrySelector.js
import React from 'react';
const tileData = [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }];
class IndustrySelector extends Component {
constructor(props) {
super(props);
this.state = { selectedIndustry: null };
}
selectIndustry(id) {
this.setState({ selectedIndustry: id });
}
isIndustrySelected(id) {
return id === this.state.selectedIndustry;
}
render() {
return (
<div>
{tileData.map((data, key) => (
<SingleTile
key={key}
{...data}
onClick={() => this.selectIndustry(data.id)}
isSelected={this.isIndustrySelected(data.id)}
/>
))}
</div>
);
}
}
The way this works is as follows.
1. Triggering the onClick handler
When a user clicks on an element in SingleTile which triggers the function from the onClick prop, this.selectIndustry in the parent component will be called with the id from the SingleTile component.
Please note that in this example, the id is remembered through a
closure. You could also pass the id as an argument to the function of
the onClick prop.
2. Setting the state in the parent component
When this.selectIndustry is called it changes the selectedIndustry key of the parent component state.
3. Updating the isSelected values form the SIngleTile components
React will automatically re-render the SingleTile components when the state of the parent component changes. By calling this.isIndustrySelected with the id of the SingleTile component, we compare the id with the id that we have stored in the state. This will thus only be equal for the SingleTile that has been clicked for the last time.
Can you post your parent component code?
It's not so important, but you can save some time by using this ES6 feature:
constructor(props){
super(props);
const {title, desc, svg, id, state} = this.props;
this.state = {
selected: false
}
}

Setting the initial state of a controlled input with a prop value that is initialized asynchronously

I have a controlled input that accepts a prop called user. This object is passed to it by its parent component, where it is initialized asynchronously with an observer1.
In the parent component:
onAuthStateChanged() {
this.unregisterAuthObserver = onAuthStateChanged((user) => {
this.setState({
isSignedIn: Boolean(user),
user,
});
});
}
I would like to populate the initial state of the controlled input with user.displayName. Something like the following, which shouldn't be an anti-pattern because the state is only dependent on the prop user on construction.
class ControlledInput extends Component {
constructor(props) {
super(props);
const { user } = props;
this.state = {
displayName: user ? user.displayName : '',
};
}
}
When I refresh the controlled input, the following problem occurs:
The value of user is undefined because the observer has yet to fire. The component is constructed and displayName is assigned to ''.
The observer fires, user is assigned a complex object, and React re-renders the component. This does not change the state of the component and displayName is still ''.
I'm stuck on how to utilize the component lifecycle methods to achieve this. Is there a best practice for this scenario? Should I even be dependent on an asynchronous prop, or should I move the observer into the controlled component?
I've considered using componentDidUpdate() to determine when user is assigned, but it feels like a hack.
Use this as an opportunity to operate on the DOM when the component has been updated. This is also a good place to do network requests as long as you compare the current props to previous props (e.g. a network request may not be necessary if the props have not changed).
1 The observer is part of Firebase Authentication, but I'm not sure that's relevant to the question.
There is no need to assign in the constructor. You should use the props provided to the render method, and following the Lifting State Up pattern handle updates to the state in the parent. That will then flow down to the child component.
See the example below. The "increment" button simulates the async call. ControlledInput is a read-only display, while ControlledInput2 allows edits to the input that are reflected in state.
class Parent extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.onInputChanged = this.onInputChanged.bind(this);
this.state = {
user,
count: 1
};
}
handleClick(e) {
const { user, count } = this.state;
this.setState({
user: { ...user, displayName: `display ${count + 1}` },
count: count + 1
});
}
onInputChanged(name) {
this.setState({
user: Object.assign(user, { displayName: name })
});
}
render() {
const { user, count } = this.state;
return (
<div>
<ControlledInput user={user} />
<div>{user.displayName}</div>
<div>Count: {count}</div>
<button onClick={this.handleClick}>increment</button>
<div>
<ControlledInput2 onInputChanged={this.onInputChanged} user={user} />
</div>
</div>
);
}
}
const ControlledInput = ({ user }) =>
<input type="text" value={user.displayName} />;
class ControlledInput2 extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onInputChanged(e.target.value);
}
render() {
const { user } = this.props;
return (
<input
type="text"
onChange={this.handleChange}
value={user.displayName}
/>
);
}
}
const user = { displayName: undefined };
const props = { user };
const rootElement = document.getElementById("sample");
ReactDOM.render(<Parent {...props} />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.2/umd/react-dom.production.min.js"></script>
<div id="sample"></div>

pass default value from graphql query in apollo to child

I am trying to pass graphql query value to my React component from the parent component's state. My method isn't work working because it's loading and isn't called again after it is done loading. Any recommendations on how to do this?
class Parent extends Component {
constructor(props) {
super(props)
const defaultSelectedId = (this.props.query && !this.props.query.loading) ?
this.props.query.defaultId : ''
this.state = {
id: defaultSelectedId
}
render() {
return(
<Child selectedId={this.state.id} />
)}}
Add
componentWillReceiveProps(nextProps) {
const defaultSelectedId = (nextProps.query && !nextProps.query.loading) ?
nextProps.query.defaultId : ''
this.setState ({
id: defaultSelectedId
})
}

React child component can't get props.object

My parent component is like this:
export default class MobileCompo extends React.Component {
constructor(props) {
super(props);
this.state = {
data: null,
datasets: {}
};
this.get_data = this.get_data.bind(this);
}
componentWillMount() {
this.get_data();
}
async get_data() {
const ret = post_api_and_return_data();
const content={};
ret.result.gsm.forEach((val, index) => {
content[val.city].push()
});
this.setState({data: ret.result.gsm, datasets: content});
}
render() {
console.log(this.state)
// I can see the value of `datasets` object
return (
<div>
<TableElement dict={d} content={this.state.data} />
<BubbleGraph maindata={this.state.datasets} labels="something"/>
</div>
)
}
}
child component:
export default class BubbleGraph extends React.Component {
constructor(props) {
super(props);
this.state = {
finalData: {datasets: []}
};
console.log(this.props);
// here I can't get this.props.maindata,it's always null,but I can get labels.It's confusing me!
}
componentWillMount() {
sortDict(this.props.maindata).forEach((val, index) => {
let tmpModel = {
label: '',
data: null
};
this.state.finalData.datasets.push(tmpModel)
});
}
render() {
return (
<div>
<h2>{this.props.labels}</h2>
<Bubble data={this.state.finalData}/>
</div>
);
}
}
I tried many times,but still don't work,I thought the reason is about await/async,but TableElement works well,also BubbleGraph can get labels.
I also tried to give a constant to datasets but the child component still can't get it.And I used this:
this.setState({ datasets: a});
BubbleGraph works.So I can't set two states at async method?
It is weird,am I missing something?
Any help would be great appreciate!
Add componentWillReceiveProps inside child componenet, and check do you get data.
componentWillReceiveProps(newProps)
{
console.log(newProps.maindata)
}
If yes, the reason is constructor methos is called only one time. On next setState on parent component,componentWillReceiveProps () method of child component receives new props. This method is not called on initial render.
Few Changes in Child component:
*As per DOC, Never mutate state variable directly by this.state.a='' or this.state.a.push(), always use setState to update the state values.
*use componentwillrecieveprops it will get called on whenever any change happen to props values, so you can avoid the asyn also, whenever you do the changes in state of parent component all the child component will get the updates values.
Use this child component:
export default class BubbleGraph extends React.Component {
constructor(props) {
super(props);
this.state = {
finalData: {datasets: []}
};
}
componentWillReceiveProps(newData) {
let data = sortDict(newData.maindata).map((val, index) => {
return {
label: '',
data: null
};
});
let finalData = JSON.parse(JSON.stringify(this.state.finalData));
finalData.datasets = finalData.datasets.concat(data);
this.setState({finalData});
}
render() {
return (
<div>
<h2>{this.props.labels}</h2>
<Bubble data={this.state.finalData}/>
</div>
);
}
}

How do I prevent React list component from refreshing when props update?

I have a list component that uses a prop to amend the subscription parameters for the purpose of limiting displayed results. This works ok, however I would like to prevent the table from entirely refreshing when I add an extra 5 results to the list. Is this possible? Currently I update the props via a method I pass down from the parent component. I've included this at the bottom of the code.
Any advice appreciated!
export default class ListingTable extends TrackerReact(React.Component) {
static propTypes = {
LimitProp: React.PropTypes.number,
}
static defaultProps = {
LimitProp: 5
}
constructor(props) {
super(props);
const subscription = Meteor.subscribe("allLists",{sort: {_id:-1}, limit: this.props.LimitProp})
this.state = {
listData: subscription,
}
}
componentWillReceiveProps(nextProps) {
if (this.props.LimitProp != nextProps.LimitProp) {
// Stop old subscription
this.state.eventsData.stop()
// Setup new subscription
const subscription = Meteor.subscribe("allLists",{sort: {_id:-1}, limit: nextProps.LimitProp})
this.setState({ listData: subscription })
}
}
...
render() {
...
<a className="item" onClick= {(event) => this.props.methodLoadMore(event)}>Load More</a>
...
}
//this method is in the parent component, onClick Method calls this
loadMore(event) {
event.stopImmediatePropagation
event.preventDefault()
var limit = this.state.listLimit;
limit = limit + 5
this.setState({listLimit: limit})
FlowRouter.setQueryParams({limit: limit})
}

Resources