nmpauls
nmpauls

Reputation: 174

Change React component visibility based on the state of another component

I have created the following React component. It uses an input box to accept a user's answer to a riddle. As soon as the user's input matches the desired answer, the input box become read-only (a bit of a strange way to use them). It also has an "isHidden" prop to determine whether the riddle is rendered.

class Riddle extends React.Component {
  constructor(props) {
    super(props);
    this.answer = props.answer.toUpperCase();
    this.state = {
      text: "",
      isAnswered: false
    };

    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event) {
    let userInput = event.target.value.toUpperCase();
    if (userInput == this.answer) {
      this.setState({
        text: userInput,
        isAnswered: true
      });
    } else {
      this.setState({
        text: userInput,
        isAnswered: false
      });
    }
  }

  render() {
    if (this.props.isHidden) {
      return <div></div>;
    } else {
      return (
        <div>
          <p>{this.props.prompt}</p>
          <input type="text" value={this.state.text} 
          readOnly={this.state.isAnswered}></input>
        </div>
      );
    }
  }
}

Here it is in practice:

function App() {
  return (
    <div>
      <Riddle prompt='The first three letters in the alphabet.' answer="abc" isHidden="false"/>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

What I would like to do is have a bunch of these riddles in sequence, but have riddles only be visible when the previous one was solved. The trouble is that I don't know how to cause the visibility update to happen.

I have read about lifting state from children to a parent component, and I've tried to see if I could create a RiddleSequence component with Riddles as its children, and have RiddleSequence manage visibility. My problem is that currently it is part of Riddle's state whether or not it's solved, and I don't know how RiddleSequence can read that information since child state should remain hidden. This seems like a reasonable way to encapsulate Riddle's functionality, but maybe I'm wrong given my goals.

I have also considered making Riddles be children of other riddles they depend on, since I can just pass state/props to children:

<Riddle prompt="first riddle"...>
  <Riddle prompt="depends on first riddle"...>
    <Riddle prompt="depends on second riddle"...>
    </Riddle>
  </Riddle>
</Riddle>

But if I have an app with 100 riddles, this seems to get ridiculous. This also reduces flexibility for a more expanded set of features (such as making one riddle depend on a group of 3 riddles).

How can I make the visibility of my Riddle components depend on the state of other riddles?

Upvotes: 2

Views: 2136

Answers (1)

Muhammed B. Aydemir
Muhammed B. Aydemir

Reputation: 1015

A simple solution would be to have a container component as you said:

class Riddle extends Component {
    constructor(props) {
        this.state = {
            text: ''
        }
        this.answer = props.answer.toUpperCase()
    }

    handleChange = event => {
        const userInput = event.target.value.toUpperCase()
        const callback = userInput == this.answer ? this.props.onSolved : undefined
        this.setState({ text: userInput }, callback)
    }

    render() {
        const { text, isAnswered } = this.state
        const { prompt } = this.props
        if (this.props.isHidden) {
            return null
        }
        return (
            <div>
                <p>{prompt}</p>
                <input type="text" value={text} readOnly={isAnswered}></input>
            </div>
        )
    }
}

and container should hold visibility like this:

class RiddleSequence extends Component {
    state = {}

    riddles = [
        {
            id: 1,
            prompt: 'The first three letters in the alphabet.',
            answer: 'abc',
            prev: null
        },
        {
            id: 2,
            prompt: 'The last three letters in the alphabet.',
            answer: 'xyz',
            prev: 1
        }
    ]

    render() {
        return (
            <div>
                {this.riddles.map(r => {
                    const { id, prev } = r
                    const visible = !prev || this.state[prev]
                    return (
                        <Riddle
                            key={id}
                            isHidden={!visible}
                            onSolved={() => this.setState({ [r.id]: true })}
                            {...r}
                        />
                    )
                })}
            </div>
        )
    }
}

Upvotes: 1

Related Questions