2

I need to render a widget which takes in settings from a SettingsPanel and passes them to a LayoutPanel (which would then re-render itself based on the updated settings). I can't seem to figure a 'clean' way to do this. Here's what I have so far:

Widget.js

class Widget extends Component {
    handleSettingsChange() {
        //need to let layout know of change
    }

    render() {
        <div>
            <SettingsPanel
                onSettingsChange={handleSettingsChange}
                initialSettings={this.props.settings}
            />
            <Layout
                data={ this.props.data}
            />
        </div>
    }
}

App.js

const settings = {
    hideImages: true,
    itemsPerPage: 5
}
<Widget settings={ settings } data={ data } />

My first thought was that, within Widget, I could do

constructor() {
    super()
    this.setState({ settings: this.props.settings });
}
handleSettingsChange(data) {
    //Assuming data is of the form {changedProperty: value}
    this.setState({ settings: Object.assign({}, this.state.settings, data) });
}
render() {
    <div>
        <SettingsPanel
            onSettingsChange={handleSettingsChange}
            initialSettings={this.props.settings}
        />
        <Layout
            data={ this.props.data}
            settings={ this.state.settings }
        />
    </div>
}

This way the parent is a dump message broker, and doesn't not have to know the specifics of what specific settings are being change. But of course, this doesn't work because, a. I'm told setting state from props is an anti-pattern b. The constructor doesn't have access to this.props anyway, and doing it else-where (componentDidMount?) feels wrong.

My second thought was to go to setProps to set props on children from the parent, but of course that's deprecated.

How do I solve this? I could probably re-architect this so this isn't a problem, but frankly I'd just like to understand what I'm missing, or what the 'react' way of solving things like this is.

This question is similar to the same problem I'm having but I'm having trouble understanding/applying what the accepted solution suggests to my problem.

tldr; "How can I make a parent component pass state from one child to another, while being ignorant of what exactly it's passing?"

HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
Naren
  • 1,910
  • 1
  • 17
  • 22

2 Answers2

2

While lukewestby's answer is correct in that it solves the problem in the original question, after further reflection I realized that that was the wrong question to ask - i.e., it was an architecture issue which is why I was having trouble with the implementation.

In this case the data-flow is [data]-> App -> Widget -> Children. "App" fetches data, so App owns it. Widget should not be able to to modify, handle changes to data it doesn't own. Hence, the callback needs to be passed to the grandparent (App), not the parent (Widget). Here's what I ended up doing.

App.js

 constructor() {
   this.state = { settings: { hideImages: true } };
 }
 handleSettingsChange(data) {
   this.setState({ settings: data });
 }
 render() {
   <Widget settings={ this.state.settings } 
      onSettingsChange={ this.handleSettingsChange }
    />
} 

Widget.js

handleDisplaySettingsChange(data) {
    this.props.onSettingsChange(data);
}
render() {
    return (
        <div>
            <SettingsPanel
                onSettingsChange={ this.handleDisplaySettingsChange }
            />
            <Layout
                {...this.props.settings }
            />
        </div>
    );
}

The advantage of doing it this way is that there is a single source of truth for settings, which is maintained by the owner (App). In the original suggestion, if on a later fetch by App the settings had changed the inner components wouldn't receive it (which explains why the React docs claim setting state from props is an anti-pattern)

Naren
  • 1,910
  • 1
  • 17
  • 22
1

It's not necessarily true that setting state from props is an anti-pattern. It seems completely reasonable to initialize a component's state from props in the constructor. Doing so doesn't degrade testability or reliability much if at all.

You can access props in the constructor as they are passed as the first parameter. You'll need to call super first, but then you can do whatever you'd like:

class Widget extends React.Component {
  constructor(props) {
    super(props);
    this.setState({ settings: props.settings });
  }
}

Then you can continue down the path you were originally headed. Another slightly more complex option would be to use a state container like Redux to handle state management and transitions for you, but only do this if you don't mind the additional learning and complexity of adding more to your app setup.

lukewestby
  • 1,207
  • 8
  • 15