React:向子组件发送点击事件的优雅方式?

huangapple go评论72阅读模式
英文:

React: elegant way to send click events to child component?

问题

以下是代码的翻译部分:

Option 1: 让子组件管理状态,允许父组件通过prop和回调来影响状态。

const Page = () => {
    const [shouldWeHideStep1, setShouldWeHideStep1] = useState(false)

    const agreeToTerms = () => {
        // 子组件中的 useEffect 捕获这个prop的变化,执行必要的操作,然后运行回调函数,将此状态变量重新设置为false。
        // 这使我们能够反复执行相同的过程。
        setShouldWeHideStep1(true)
    }

    return (
        <div>
            <Collapsible 
                openDefault={true} 
                title="Step1: Terms" 
                toggleClose={shouldWeHideStep1} 
                toggleCloseDone={() => {
                    setShouldWeHideStep1(false)
                }}>
              <div>
                  这里是条款: 等等...
                  <button onClick={agreeToTerms}>同意并进入下一步</button>
              </div>
            </Collapsible>
            <Collapsible openDefault={false} title="Step2: Something">
              <div>
                  这是另外一些内容...
              </div>
            </Collapsible>
        </div>
    )
}

const Collapsible = ({openDefault, toggleClose = false, toggleCloseDone = null}) => {
    const [open, setOpen] = useState(openDefault)

    useEffect(() => {
        // 父组件向我们发送一次性“关闭”事件。
        // 我们在这里执行关闭操作,更新本地状态,然后回调父组件的setState方法。
        if(toggleClose === true){
            setOpen(false)
            if(toggleCloseDone){
                toggleCloseDone()
            }
        }
    }, [toggleClose])

    const toggle = () => {
        setOpen(!open)
    }

    return (
        <div className={styles.collapsible}>
            <div className={styles.collapsibleTitle} onClick={toggle}>
                {title}
            </div>
            {open && (
                <div className={styles.collapsibleContent}>
                    {children}
                </div>
            )}
        </div>
    )
}

Option 2: 从子组件中移除状态,全部在父组件中管理。

const Page = () => {
    const [step1Open, setStep1Open] = useState(true)
    const [step2Open, setStep2Open] = useState(false)

    const agreeToTerms = () => {
        setStep1Open(false)
        setStep2Open(true)
    }

    return (
        <div>
            <Collapsible
                open={step1Open}
                title="Step1: Terms"
                toggleCallback={() => {
                    setStep1Open(!step1Open)
                }}>
              <div>
                  这里是条款: 等等...
                  <button onClick={agreeToTerms}>同意并进入下一步</button>
              </div>
            </Collapsible>
            <Collapsible
                openDefault={false}
                title="Step2: Something"
                open={step2Open}
                toggleCallback={() => {
                    setStep2Open(!step2Open)
                }}>
              <div>
                  这是另外一些内容...
              </div>
            </Collapsible>
        </div>
    )
}

const Collapsible = ({open, toggleCallback}) => {
    return (
        <div className={styles.collapsible}>
            <div className={styles.collapsibleTitle} onClick={toggleCallback}>
                {title}
            </div>
            {open && (
                <div className={styles.collapsibleContent}>
                    {children}
                </div>
            )}
        </div>
    )
}
英文:

This sounds like an incredible basic question, I'm looking for guidance on the best pattern to follow for this problem.

An example: clicking a button an accordion will collapse it.

This seemed simple enough, just pass in an open prop and have the parent manage state. However this fell apart when wanting to also manage that open state from the child component (i.e. clicking to open/close the accordion title).

I've got two basic examples below:

Option 1: Let child manage state, allow parent to optionally impact that state with a prop and callback.


const Page = () =&gt; {
const [shouldWeHideStep1, setShouldWeHideStep1] = useState(false)
const agreeToTerms = () =&gt; {
// A useEffect in child component picks up this prop change, does what it needs to do, and then
// runs our callback which will set this state variable back to false.
// This allows us to repeat the same process over and over again.
setShouldWeHideStep1(true)
}
return (
&lt;div&gt;
&lt;Collapsible 
openDefault={true} 
title=&quot;Step1: Terms&quot; 
toggleClose={shouldWeHideStep1} 
toggleCloseDone={() =&gt; {
setShouldWeHideStep1(false)
}}&gt;
&lt;div&gt;
Here are the terms: etc..
&lt;button onClick={agreeToTerms}&gt;Agree and go to next step&lt;/button&gt;
&lt;/div&gt;
&lt;/Collapsible&gt;
&lt;Collapsible openDefault={false} title=&quot;Step2: Something&quot;&gt;
&lt;div&gt;
Here is something else...
&lt;/div&gt;
&lt;/Collapsible&gt;
&lt;/div&gt;
)
}
const Collapsible = ({openDefault, toggleClose = false, toggleCloseDone = null}) =&gt; {
const [open, setOpen] = useState(openDefault)
useEffect(() =&gt; {
// Our parent component is sending us a one off &quot;time to close&quot; event.
// We do our close thing here, update local state, then we call back to the parent
// setState method to 
if(toggleClose === true){
setOpen(false)
if(toggleCloseDone){
toggleCloseDone()
}
}
}, [toggleClose])
const toggle = () =&gt; {
setOpen(!open)
}
return (
&lt;div className={styles.collapsible}&gt;
&lt;div className={styles.collapsibleTitle} onClick={toggle}&gt;
{title}
&lt;/div&gt;
{open &amp;&amp; (
&lt;div className={styles.collapsibleContent}&gt;
{children}
&lt;/div&gt;
)}
&lt;/div&gt;
)
}

Option 2: Remove state from child, manage it all in parent component


const Page = () =&gt; {
const [step1Open, setStep1Open] = useState(true)
const [step2Open, setStep2Open] = useState(false)
const agreeToTerms = () =&gt; {
setStep1Open(false)
setStep2Open(true)
}
return (
&lt;div&gt;
&lt;Collapsible
open={step1Open}
title=&quot;Step1: Terms&quot;
toggleCallback={() =&gt; {
setStep1Open(!step1Open)
}}&gt;
&lt;div&gt;
Here are the terms: etc..
&lt;button onClick={agreeToTerms}&gt;Agree and go to next step&lt;/button&gt;
&lt;/div&gt;
&lt;/Collapsible&gt;
&lt;Collapsible
openDefault={false}
title=&quot;Step2: Something&quot;
open={step2Open}
toggleCallback={() =&gt; {
setStep2Open(!step2Open)
}}&gt;
&lt;div&gt;
Here is something else...
&lt;/div&gt;
&lt;/Collapsible&gt;
&lt;/div&gt;
)
}
const Collapsible = ({open, toggleCallback}) =&gt; {
return (
&lt;div className={styles.collapsible}&gt;
&lt;div className={styles.collapsibleTitle} onClick={toggleCallback}&gt;
{title}
&lt;/div&gt;
{open &amp;&amp; (
&lt;div className={styles.collapsibleContent}&gt;
{children}
&lt;/div&gt;
)}
&lt;/div&gt;
)
}

What is the preferred approach for this type of problem? There's likely many more ways than the above two as well.

答案1

得分: 1

代码部分不需要翻译,以下是代码外的翻译部分:

"It always depends, but from what I see that you're trying to do, the state has to be outside the Collapsible component in order to allow one of the Collapsible's children to modify the visibility."

"Option 1 just doesn't seem right to me since you still have to have state in the parent to track when the first Collapsible should be hidden, and it is an odd way of managing the state."

"If I have to choose between option 1 and 2, I would pick #2 with a few modifications:

  • Since you want to open the next collapsible when the previous one closes, using useReducer would be much better for doing that synchronization and it would also allow you to add more collapsibles without creating a new state for every collapsible
  • Remove unused prop openDefault since open already does that"

下面是代码中的一部分,我将保留原样:

const Collapsible = ({ open, toggleCallback, title, children }) =&gt; {
  // remove -- used for testing purposes
  const styles = {
    collapsibleTitle: 'test',
    title: 'test',
  };

  return (
    <div className={styles.collapsible}>
      <div className={styles.collapsibleTitle} onClick={toggleCallback}>
        {title}
      </div>
      {open && <div className={styles.collapsibleContent}>{children}</div>}
    </div>
  );
};

export const Page = () =&gt; {
  // Setting the initial visibility for all the collapsibles here
  const initialCollapsiblesVisibility = {
    step1: true,
    step2: false,
  };

  // The reducer will allow us to synchronize the collapsible components depending
  // the action that happens
  function reducer(prev, action) {
    switch (action.type) {
      // just toggle step1
      case 'toggle1': {
        return {
          ...prev,
          step1: !prev.step1,
        };
      }
      // just toggle step2
      case 'toggle2': {
        return {
          ...prev,
          step2: !prev.step2,
        };
      }

      // When we agree to terms, open collapsible2
      case 'agree1': {
        return {
          step1: false,
          step2: true,
        };
      }
      default: {
        throw Error('Unknown action: ' + action.type);
      }
    }
  }

  const [stepsVisibility, setStepsVisibility] = useReducer(reducer, initialCollapsiblesVisibility);

  return (
    <div>
      <Collapsible
        open={stepsVisibility.step1}
        title="Step1: Terms"
        toggleCallback={() => {
          setStepsVisibility({ type: 'toggle1' });
        }}
      >
        <div>
          Here are the terms: etc..
          <button onClick={() => setStepsVisibility({ type: 'agree1' })}>Agree and go to next step</button>
        </div>
      </Collapsible>
      <Collapsible
        openDefault={false}
        title="Step2: Something"
        open={stepsVisibility.step2}
        toggleCallback={() => {
          setStepsVisibility({ type: 'toggle2' });
        }}
      >
        <div>Here is something else...</div>
      </Collapsible>
    </div>
  );
};

希望这有助于您理解代码的功能和结构。

英文:

It always depends, but from what I see that you're trying to do, the state has to be outside the Collapsible component in order to allow one of the Collapsible's children to modify the visibility.

Option 1 just doesn't seem right to me since you still have to have state in the parent to track when the first Collapsible should be hidden, and it is an odd way of managing the state.

If I have to choose between option 1 and 2, I would pick #2 with a few modifications:

  • Since you want to open the next collapsible when the previous one closes, using useReducer would be much better for doing that synchronization and it would also allow you to add more collapsibles without creating a new state for every collapsible
  • Remove unused prop openDefault since open already does that

...

const Collapsible = ({ open, toggleCallback, title, children }) =&gt; {
// remove -- used for testing purposes
const styles = {
collapsibleTitle: &#39;test&#39;,
title: &#39;test&#39;,
};
return (
&lt;div className={styles.collapsible}&gt;
&lt;div className={styles.collapsibleTitle} onClick={toggleCallback}&gt;
{title}
&lt;/div&gt;
{open &amp;&amp; &lt;div className={styles.collapsibleContent}&gt;{children}&lt;/div&gt;}
&lt;/div&gt;
);
};
export const Page = () =&gt; {
// Setting the initial visibility for all the collapsibles here
const initialCollapsiblesVisibility = {
step1: true,
step2: false,
};
// The reducer will allow us to synchronize the collapsible components depending
// the action that happens
function reducer(prev, action) {
switch (action.type) {
// just toggle step1
case &#39;toggle1&#39;: {
return {
...prev,
step1: !prev.step1,
};
}
// just toggle step2
case &#39;toggle2&#39;: {
return {
...prev,
step2: !prev.step2,
};
}
// When we agree to terms, open collapsible2
case &#39;agree1&#39;: {
return {
step1: false,
step2: true,
};
}
default: {
throw Error(&#39;Unknown action: &#39; + action.type);
}
}
}
const [stepsVisibility, setStepsVisibility] = useReducer(reducer, initialCollapsiblesVisibility);
return (
&lt;div&gt;
&lt;Collapsible
open={stepsVisibility.step1}
title=&quot;Step1: Terms&quot;
toggleCallback={() =&gt; {
setStepsVisibility({ type: &#39;toggle1&#39; });
}}
&gt;
&lt;div&gt;
Here are the terms: etc..
&lt;button onClick={() =&gt; setStepsVisibility({ type: &#39;agree1&#39; })}&gt;Agree and go to next step&lt;/button&gt;
&lt;/div&gt;
&lt;/Collapsible&gt;
&lt;Collapsible
openDefault={false}
title=&quot;Step2: Something&quot;
open={stepsVisibility.step2}
toggleCallback={() =&gt; {
setStepsVisibility({ type: &#39;toggle2&#39; });
}}
&gt;
&lt;div&gt;Here is something else...&lt;/div&gt;
&lt;/Collapsible&gt;
&lt;/div&gt;
);
};

huangapple
  • 本文由 发表于 2023年3月12日 12:16:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/75711014.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定