首页 > 解决方案 > 如何用嵌套的孩子填充状态

问题描述

我想像这样填充状态:

state = {
    id: '1',
    parentId: null,
    text: '', // state of the root input
    child: [
        {
            id: '1.1',
            parentId: '1',
            text: '', // State of the first child input
            child: null // First child didn't have any children
        },
        {
            id: '1.2',
            parentId: '1',
            text: '', // State of the second child input
            child: null // Second child didn't have any children
        }],
};

故事:

这是一个递归问题。我正在做的是ul只有render一个项目,即一个<Field />组件。

<Field />组件也有一个状态,一个名为id和渲染input元素和<Quantity />组件的道具。

<Quantity />组件生成嵌套列表项,它们只不过是一个<Field />组件。

生成<Field />的组件具有正确的状态结构,如正确idparentId但主要问题是嵌套生成的组件有自己的范围,不关心其父<Field />组件状态。

我用错误的模式解决了这个问题吗?

我是否必须从一个<List />呈现一个无状态组件的有状态组件开始<Field />,并将所有父状态作为该<Field />组件的道具传递?

但是如何跟踪状态?

沙盒链接

应用程序.jsx:

import React from 'react';
import {Field} from './components/Field';

    const App = () => {
        return (<ul className="list"><Field id='1'/></ul>);

    };

export default App;

组件/Field.jsx:

import React, {Component} from 'react';
import {Quantity} from './Quantity';

export class Field extends Component {

    /**
     * @type {{id: string, text: string, parentId: (string|null), child: ([]|null)}}
     */
    state = {
        id: this.props.id,
        parentId: null,
        child: null,
        text: '',
    };

    /**
     * Handle onchange behavior of the input
     * @param target
     */
    onChangeHandler = ({target}) => {
        const {name, value} = target;
        this.setState(prevState => {
            return {[name]: value};
        });
    };


    /**
     * Trigger this function when Button of `Quantity component` is clicked
     * @param {Number} quantity - Generate array of objects with properties
     * and update state
     */
    populateChild = (quantity) => {
        // Construct new array of objects
        const data = [...Array(quantity)].map((_, index) => {
            // id => 1.1 .. 1.2 .. 1.3
            // parentId => 1
            const id = this.state.id + '.' + (index + 1);
            return {...this.state, id: id, parentId: this.state.id}
        });


        this.setState(prevState => {
            return {child: data};
        });
    };


    render() {
        return (<li>
            <div className="wrap">
                <div>
                    <label>Enter Text:</label>
                    <input type="text" name="text" onChange={this.onChangeHandler} value={this.state.text}/>
                </div>
                <div>
                    <Quantity funRef={this.populateChild}/>
                    <br/>
                    <br/>
                </div>
            </div>
            {/* Generate New unordered list only if `child` property of the state is changed.   */}
            {this.state.child !== null &&
            <ul>{this.state.child.map(element => <Field key={element.id} {...element}/>)}</ul>}
        </li>);
    };

}

组件/数量.jsx:

import React, {Component,Fragment} from 'react';

export class Quantity extends Component {
    /**
     * @type {{min: number, quantity: number}}
     */
    state = {
        min: 0,
        quantity:0
    };


    /**
     * Handle onchange behavior of the input
     * @param target
     */
    onChangeHandler = ({target}) => {
        const val = parseInt(target.value);
        this.setState(prevState => {
            return {quantity: val};
        });
    };



    render() {
        return (
            <Fragment>
                <label>Children Quantity</label>
                <input type="number" min={this.state.min} onChange={this.onChangeHandler} value={this.state.quantity} />
                <button onClick={ () => { this.props.funRef(this.state.quantity) } }>Generate</button>
            </Fragment>
        );

    }
}

标签: reactjs

解决方案


这是我的 stackblitz 解决方案,请注意项目state被管理到顶部App组件中

我决定将项目存储到深度 = 1 的数组中,这样更容易处理更新:

items = [
  {
    id: "1",
    parentId: null,
    text: ""
  },
  {
    id: "1.1",
    parentId: "1",
    text: ""
  },
  {
    id: "1.2",
    parentId: "1",
    text: ""
  }
];

这是App组件:

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
     items: defaultItems
    };
  }

  handleChangeText = (id, text) => {
    this.setState(prevState => ({
      items: prevState.items.map(
        item => item.id === id ? {...item, text} : item 
      )
    }));
  };

  handleAddChildren = (parentId, quantity) => {
    this.setState(prevState => {
      const childrenItems = prevState.items
        .filter(item => item.parentId === parentId);

      const otherItems = prevState.items
        .filter(item => item.parentId !== parentId);

      const childrenText = childrenItems.map(item => item.text);

      const newChildrenItems = Array.from(Array(quantity))
        .map((_, index) => ({
          parentId,
          id: `${parentId}.${index + 1}`,
          text: childrenText[index] || ""
      }));

      return {
        items: [...otherItems, ...newChildrenItems]
      };
    });
  };

  getChildren = parentId => this.state.items.filter(
      item => item.parentId === parentId
  );

  render() {
    const {items} = this.state;
    return (
      <ul>
        {
          this.getChildren(null).map(item => 
            <Field
              key={item.id}
              item={item}
              getChildren={this.getChildren}
              onChangeText={this.handleChangeText}
              onAddChildren={this.handleAddChildren}
             /> 
          )
        }
      </ul>
    );
  }
}

这是  Field组件:

export class Field extends Component {

  handleChangeText = e => {
    const {onChangeText, item} = this.props;
    onChangeText(item.id, e.target.value);
  };

  handleGenerate = quantity => {
    const {onAddChildren, item} = this.props;
    onAddChildren(item.id, quantity);
  };

  render() {
    const {item, getChildren, onChangeText, onAddChildren} = this.props;
    const children = getChildren(item.id);

    return (
      <li>
        <div className="wrap">
          <div>
            <label>Enter Text:</label>
            <input
              type="text"
              name="text"
              onChange={this.handleChangeText}
              value={item.text}
            />
          </div>
          <div>
            <Quantity onGenerate={this.handleGenerate} />
            <br />
            <br />
          </div>
        </div>
        {children.length > 0 && (
          <ul>
            {children.map(item => (
              <Field
                key={item.id}
                item={item}
                getChildren={getChildren}
                onChangeText={onChangeText}
                onAddChildren={onAddChildren}
              />
            ))}
          </ul>
        )}
      </li>
    );
  }
}

请注意,我们可以创建一个React context来避免将属性getChildren, onChangeText,传递onAddChildrenField组件。这些属性实际上引用了顶层App组件的函数并向下传递给嵌套Field组件

这是使用的解决方案React.context

您只需要创建一个上下文:

const AppContext = React.createContext();

然后,您需要将上下文值存储到状态中,并使用上下文包装您的应用程序。现在你只需要传递道具和key组件。ìtemField

constructor(props) {
  super(props);

  this.state = {
    items: defaultItems,
    contextValue: {
      onChangeText: this.onChangeText,
      onAddChildren: this.onAddChildren,
      getChildren: this.getChildren
    }
  };

}

...

render() {
  const {items, contextValue} = this.state;
  const {Provider} = AppContext;
  return (
    <Provider value={contextValue}>
      <ul>
        {
          this.getChildren(null).map(
            item => <Field key={item.id} item={item}/> 
          )
        }
      </ul>
    </Provider>
  );

}

Field您可以检索上下文的组件中,您现在可以从上下文中static contextType = AppContext;检索函数onChangeText,onAddChildrengetChildren

export class Field extends Component {
 static contextType = AppContext;

 handleChangeText = e => {
   const {item} = this.props;
   const {onChangeText} = this.context;

   onChangeText(item.id, e.target.value);
 };

 handleGenerate = quantity => {
   const {item} = this.props;
   const {onAddChildren} = this.context;

   onAddChildren(item.id, quantity);
 };

 render() {
   const {item} = this.props;
   const children = this.context.getChildren(item.id);

   return (
     <li>
       <div className="wrap">
         <div>
           <label>Enter Text:</label>
           <input
             type="text"
             name="text"
             onChange={this.handleChangeText}
             value={item.text}
           />
         </div>
         <div>
           <Quantity onGenerate={this.handleGenerate} />
           <br />
           <br />
          </div>
       </div>
       {children.length > 0 &&
         <ul>{children.map(item => <Field key={item.id} item={item}/>)}</ul>
       }
     </li>
   );
 }
}

推荐阅读