javascript - 在子安装上设置父状态是一种反模式吗?

标签 javascript reactjs

我正在用 React 创建一个表单,并创建了一个 <Field />需要根据特定类型的子元素数量呈现不同包装元素的组件。

例如,如果字段包装单个输入,它应该呈现一个包装器 <div />和一个 <label /> .但是,如果它包装多个输入,它应该呈现一个 <fieldset />。和一个 <legend /> .

至关重要的是, child 不一定是 <Field /> 的直系后代组件,所以计算 childrenReact.Children.count行不通。

我可以通过在子输入挂载时设置父字段状态来轻松完成此操作,例如:

const FormFieldContext = createContext({});

// Simplified Field component
const Field = ({ label, children, ...props }) => {
  const [fieldCount, setFieldCount] = useState(0);
  const Wrapper = fieldCount > 1 ? 'fieldset' : 'div';
  const Label = fieldCount > 1 ? 'legend' : 'label';

  return (
    <Wrapper>
      <Label>{label}</Label>
      <FormFieldContext.Provider value={{ setFieldCount }}>
        {children}
      </FormFieldContext.Provider>
    </Wrapper>
  );
};

// Inside <Checkbox />
const Checkbox = ({ name, ...props }) => {
  const { setFieldCount } = useContext(FormFieldContext);

  useLayoutEffect(() => {
    setFieldCount(count => count + 1);

    return () => {
      setFieldCount(count => count - 1);
    };
  }, [setFieldCount, name]);

  return ( /** etc */ );
};


但是我的直觉告诉我这是一个反模式,因为:

  • 它会导致立即重新渲染。
  • 大概不可能让 SSR 变得友好,因为你依赖于基于坐骑的副作用。这意味着 SSR 版本不会考虑 child 。

放弃这个并强制消费者手动设置一个 isFieldset 是否更好?支持 <Field />成分?还是有更聪明的方法来实现这一点?

期望的用法:

{# Renders a div and a label #}
<Field name="email" label="Enter your email">
  <TextInput type="email" />
</Field>

{# Renders a legend and a fieldset #}
<Field name="metal" label="Select your metals">
  <Checkbox label="Bronze" value="bronze" />
  <Checkbox label="Silver" value="silver" />
</Field>
<div class="form-field">
  <label for="email">Enter your email</label>
  <input type="email" id="email" />
</div>

<fieldset class="form-field">
  <legend>Select your metals</label>
  <label>
    <input type="checkbox" name="metal" value="bronze" /> Bronze
  </label>
  <label>
    <input type="checkbox" name="metal" value="silver" /> Silver
  </label>
</fieldset>

最佳答案

实际上可以从 children 中递归获取 React 组件树中的所有子项 Prop ,当你有所有的 child 时,你可以计算有多少类型的组件 CheckBox等等,以确定应该如何包裹 child 。

可能还有更优雅的方法,但在下面的代码示例中我 .reduce使用 getChildrenRecursively 将直接子级转换为包含所有 个子级的扁平数组.

const getChildrenRecursively = (soFar, parent) => {
    if(typeof parent === "string") return soFar.concat(parent);

    const children = Array.isArray(parent.props.children) ? 
    parent.props.children : 
    React.Children.toArray(parent.props.children);

  const childCount  = children.length;

    if(childCount <= 0) {
    return soFar.concat(parent);
  } else {
    return soFar.concat([parent], children.flatMap(child => getChildrenRecursively([], child)));
  }
}

不能 100% 确定此解决方案与您的状态逻辑等配合使用的效果如何,但我认为您会发现它很有用。

const getChildrenRecursively = (soFar, parent) => {
	if(typeof parent === "string") return soFar.concat(parent);
  
	const children = Array.isArray(parent.props.children) ? 
  	parent.props.children : 
    React.Children.toArray(parent.props.children);
  
  const childCount  = children.length;
  
	if(childCount <= 0) {
  	return soFar.concat(parent);
  } else {
  	return soFar.concat([parent], children.flatMap(child => getChildrenRecursively([], child)));
  }
}

const MyInput = ({placeholder} ) => <input placeholder={placeholder} ></input>;


const MyComp = ({children}) => {
	const childArr = React.Children.toArray(children);
  // get all children in hierarcy
  const flattenedChildren = childArr.reduce(getChildrenRecursively, []);
  
  const numberOfInputs = flattenedChildren
  	.filter(child => child.type && child.type.name === "MyInput").length;
  
  const Wrapper = numberOfInputs > 1 ? 'fieldset' : 'div';

  
	return (
  	<div>
      	<Wrapper className="form-element">
          <legend>{numberOfInputs} input(s)</legend>
          <div>{children}</div>
        </Wrapper>
    </div>
  )
}


const App = () => {
	return (
      <div>
        <MyComp>
          <MyInput placeholder="Some input #1" />
          <div>
            <span>Some non-input element</span>
          </div>
        </MyComp>
        <MyComp>
          <MyInput placeholder="Some input #1" />
          <div>
            <div>
               <MyInput placeholder="Some nested input #2" />
            </div>
            <div>
              <span>Some non-input element</span>
            </div>
          </div>
        </MyComp>
      </div>
   )
}

ReactDOM.render(<App />, document.querySelector("#app"));
fieldset {
	margin: 0px 0px 10px 0px;
	border: 1px solid teal;
	padding: 20px;
}

div.form-element {
  border: 1px solid teal;
  padding: 20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>

关于javascript - 在子安装上设置父状态是一种反模式吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57252830/

相关文章:

javascript - Bootstrap 轮播,每张幻灯片和 AngularJS 有多个元素

javascript - 使用向下/向上滑动过渡动画 v-if (Vue.js 2)

reactjs - FormatJS/React-intl 中的 If/else

javascript - 删除组件之间的空间

reactjs - 在 React 应用程序中嵌入 Typeform

javascript - HTML DOM 位置刷新率

javascript - 扩展类或创建新函数,哪个更好?

javascript - Google-Maps-React 与 TypeScript。参数错误

javascript - 由于某种原因在 native react 中渲染后发生 ComponentWillMount

javascript - 如何在表单中添加按钮,在提交时检查必填字段但不刷新页面?