简体   繁体   中英

React Hooks: set component state without re-rendering twice

So I have two components: an Input component which is basically just a button that sets the current status of the input value to active and then sends a value object to its parent component Question:

import React, { useState, useEffect } from 'react';
import './Input.css';

const Input = (props) => {
    // input state:
    const title = props.title;
    const index = props.index;
    const [active, setActive] = useState(false);
    const [inputValue, setInputValue] = useState({index, title, active});

// sets active status based on what status is in Question component
// the logic there would only allow 1 radio input to be active as opposed to checkboxes where we have multiple active
useEffect(() => {
    setActive(props.active);
}, [props.active]);


// stores activity status of single input and re-runs only when 'active' changes (when clicking the button)
useEffect(() => {
    setInputValue({index, title, active});
}, [active]);

// returns updated input value to Question component
useEffect(() => {
    return props.selected(inputValue);
}, [inputValue]);

return (
    <div className='input'>
        <button 
            data-key={title}
            className={props.active ? 'highlight' : ''}
            onClick={() => setActive(active => !active)}
        >
            {title}
        </button>
    </div>
);
}

export default Input;

And Question checks if the current question type (which it receives from another parent component) is a 'radio' button type in which case you can only have one option. So currently I set it up like this:

import React, { useState, useEffect } from 'react';
import s from './Question.css';
import Input from './Input/Input';

const Question = (props) => {
    // create intitial state of options
    let initialState = [];
    for (let i=0; i < props.options.length; i++) {
        initialState.push(
            {
                index: i,
                option: props.options[i],
                active: false,
            }
        )
    }

    // question state:
    let questionIndex = props.index;
    let questionActive = props.active;
    let questionTitle = props.question;
    let questionType = props.type;
    let [questionValue, setQuestionValue] = useState(initialState);
    let [isAnswered, setIsAnswered] = useState(false);

    useEffect(() => {
        console.log(questionValue);
    }, [questionValue]);

    // stores currently selected input value for question and handles logic according to type
    const storeInputValue = (inputValue) => {
        let questionInputs = [...questionValue];
        let index = inputValue.index;

        // first set every input value to false when type is radio, with the radio-type you can only choose one option
        if (questionType === 'radio') {
            for (let i=0; i < questionInputs.length; i++) {
                questionInputs[i].active = false;
            }
        }

        questionInputs[index].active = inputValue.active;
        setQuestionValue([...questionInputs]);

        // set state that checks if question has been answered
        questionValue.filter(x => x.active).length > 0 ? setIsAnswered(true) : setIsAnswered(false);
    }

    // creates the correct input type choices for the question
    let inputs = [];
    for (const [index, input] of props.options.entries()) {
        inputs.push(
            <Input
                key={index}
                index={index}
                title={input}
                active={questionValue[index].active}
                selected={storeInputValue}
            />
         );
    }

    // passes current state (selected value) and the index of question to parent (App.js) component
    const saveQuestionValue = (e) => {
        e.preventDefault();
        props.selection(questionValue, questionIndex, questionTitle);
    }

    return (
        <div className={`question ${!questionActive ? 'hide' : ''}`}>
            <h1>{props.question}</h1>
            <div className="inputs">
                {inputs}
            </div>
            <a className={`selectionButton ${isAnswered ? 'highlight' : ''}`} href="" onClick={e => saveQuestionValue(e)}>
                <div>Save and continue -></div>
            </a>
        </div>
    );
}

export default Question;

With this setup when I click on an input it sends that to the Question component and that one returns a prop.active to Input so it highlights the input value. But when I click a new input it re-renders twice since it listens to the active state changing in Input, and sets all inputs to false.

My question is: how can I set up the logic in this code to act like a radio input so it only sets the currently selected input to active instead of first setting every input to active = false?

You should not be duplicating the value of state in two different components. There should always only be a single source of truth for what a value is in state. This is one of the most important patterns of React and is even mentioned in the official documentation .

Instead, you should lift shared state up so that it lives at the "closest common ancestor". Your <Input> components should not have any internal state at all - they should be pure functions that do nothing except render the current value and provide a callback to update said value. But, they do not store that value themselves - it is passed to them as a prop.

All of the logic for which input is active and what value it has should live in the parent component and be passed down from there. Whenever you are setting state in a child component and then somehow passing that state back up to a parent is a warning flag, because in React the data should flow down .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM