在子装载上设置父状态是否是一种反模式?



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

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

至关重要的是,孩子不一定是<Field />组件的直系后代,因此用React.Children.count计算children是行不通的。

我可以通过在装载子输入时设置父字段状态来轻松实现此目的,例如:

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版本不会考虑子版本。

放弃这一点并强制消费者在<Field />组件上手动设置isFieldset道具更好吗?或者有没有更聪明的方法来实现这一点?

期望用法:

{# 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>

实际上可以递归地从你的childrenprops 中获取 React 组件树中的所有子组件,当你拥有所有子组件时,你可以计算有多少个类型的CheckBox等组件来确定应该如何包装子组件。

可能有更优雅的方法,但在下面的代码示例中,我将直接子级.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>

相关内容

最新更新