網站搜尋

JavaScript - 如何在 React 中存取子節點的狀態


我有以下結構:

FormEditor - 包含多個 FieldEditor 實例 FieldEditor - 編輯表單的欄位並在其狀態中保存該欄位的各種值

當在 FormEditor 中點擊按鈕時,我希望能夠收集所有 FieldEditor 組件中的欄位資訊,這些資訊存在它們的狀態中,並將所有資訊集中在 FormEditor 中。

我考慮過將關於欄位的信息儲存在 FieldEditor 的 state 之外,而是放在 FormEditor 的 state 中。然而,那樣的話 FormEditor 就需要監聽它每個 FieldEditor 組件的變化,並將它們的信息存儲在自己的 state 中。

我不能直接進入兒童州嗎?這理想嗎?

答案 #1

在我詳細說明如何訪問子組件的狀態之前,請務必閱讀 Markus-ipse 對於處理這種特定情況的更好解決方案的回答。

如果你確實希望訪問一個組件子元素的狀態,你可以給每個子元素分配一個名為 ref 的屬性。現在有兩種方法來實現引用:使用 React.createRef() 和回調引用。

使用 React.createRef()

這是截至 React 16.3 版本目前推薦使用引用的方式(更多資訊請參閱文件)。如果你使用的是較早版本,請參考下方有關回調引用的說明。

你需要在父組件的建構函數中創建一個新的參考,然後通過 ref 屬性將其分配給子組件。

class FormEditor extends React.Component {
  constructor(props) {
    super(props);
    this.FieldEditor1 = React.createRef();
  }
  render() {
    return <FieldEditor ref={this.FieldEditor1} />;
  }
}

為了訪問這種類型的引用,您需要使用:

const currentFieldEditor1 = this.FieldEditor1.current;

這將返回掛載組件的一個實例,因此您可以使用 currentFieldEditor1.state 來訪問其狀態。

簡單說明,如果你在 DOM 節點使用這些參考而非元件(例如 <div ref={this.divRef} />),那麼 this.divRef.current 會回傳底層的 DOM 元素,而非元件實例。

回撥裁判

此特性會接收一個回調函式,該函式會傳遞給所附元件的參考。此回調會在元件掛載或卸載後立即執行。

例如:

<FieldEditor
    ref={(fieldEditor1) => {this.fieldEditor1 = fieldEditor1;}
    {...props}
/>

在這些例子中,參考被存儲在父組件中。要在你的程式碼中調用這個組件,你可以使用:

this.fieldEditor1

然後使用 this.fieldEditor1.state 來獲取狀態。

有一點需要注意,確保你的子元件已經渲染完畢,然後再嘗試訪問它 ^_^

如上所述,如果你在 DOM 節點而不是組件上使用這些引用(例如 <div ref={(divRef) => {this.myDiv=divRef;}} />),那麼 this.divRef 將返回底層的 DOM 元素,而不是組件實例。

進一步資訊

如果你想閱讀更多關於 React 的 ref 屬性的內容,可以查看 Facebook 的這個頁面。

請務必閱讀「不要過度使用 refs」部分,其中說明你不應該使用子組件的 state 來「讓事情發生」。

答案#2

如果你已經為各個 FieldEditors 有一個 onChange 處理函數,我不明白為什麼你不能只是將狀態提升到 FormEditor 組件,然後從那裡傳遞一個回調給 FieldEditors,以更新父組件的狀態。對我來說,這似乎是一種更符合 React 風格的做法。

也許大概是這樣的意思:

const FieldEditor = ({ value, onChange, id }) => {
  const handleChange = event => {
    const text = event.target.value;
    onChange(id, text);
  };

  return (
    <div className="field-editor">
      <input onChange={handleChange} value={value} />
    </div>
  );
};

const FormEditor = props => {
  const [values, setValues] = useState({});
  const handleFieldChange = (fieldId, value) => {
    setValues({ ...values, [fieldId]: value });
  };

  const fields = props.fields.map(field => (
    <FieldEditor
      key={field}
      id={field}
      onChange={handleFieldChange}
      value={values[field]}
    />
  ));

  return (
    <div>
      {fields}
      <pre>{JSON.stringify(values, null, 2)}</pre>
    </div>
  );
};

// To add the ability to dynamically add/remove fields, keep the list in state
const App = () => {
  const fields = ["field1", "field2", "anotherField"];

  return <FormEditor fields={fields} />;
};

原始版本 - 預掛鉤版:

class FieldEditor extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event) {
    const text = event.target.value;
    this.props.onChange(this.props.id, text);
  }

  render() {
    return (
      <div className="field-editor">
        <input onChange={this.handleChange} value={this.props.value} />
      </div>
    );
  }
}

class FormEditor extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};

    this.handleFieldChange = this.handleFieldChange.bind(this);
  }

  handleFieldChange(fieldId, value) {
    this.setState({ [fieldId]: value });
  }

  render() {
    const fields = this.props.fields.map(field => (
      <FieldEditor
        key={field}
        id={field}
        onChange={this.handleFieldChange}
        value={this.state[field]}
      />
    ));

    return (
      <div>
        {fields}
        <div>{JSON.stringify(this.state)}</div>
      </div>
    );
  }
}

// Convert to a class component and add the ability to dynamically add/remove fields by having it in state
const App = () => {
  const fields = ["field1", "field2", "anotherField"];

  return <FormEditor fields={fields} />;
};

ReactDOM.render(<App />, document.body);

答案#3

如同先前的回答所說,試著將狀態移到一個頂層元件,並透過傳給子元件的回呼函式修改該狀態。

如果你真的需要存取被宣告為函式元件(hooks)的子狀態,可以在父元件宣告一個 ref,然後把它當作 ref 屬性傳給子元件,但你需要先用 React.forwardRef,然後用 hook useImperativeHandle 來宣告一個你可以在父元件呼叫的函式。

看看以下的例子:

const Parent = () => {
    const myRef = useRef();
    return <Child ref={myRef} />;
}

const Child = React.forwardRef((props, ref) => {
    const [myState, setMyState] = useState('This is my state!');
    useImperativeHandle(ref, () => ({getMyState: () => {return myState}}), [myState]);
})

那麼你應該可以透過呼叫來在父元件中取得 myState:

myRef.current.getMyState();

答案#4

現在是2020,很多人會來這裡尋找類似的解決方案,但使用Hooks(它們很棒!)以及在代碼整潔度和語法方面的最新方法。

正如之前的回答所述,處理這種問題的最佳方法是將狀態保存在子元件 fieldEditor 之外。你可以用多種方式來做到這一點。

最「複雜」的是具有全局上下文(狀態),父組件和子組件都可以訪問和修改它。當組件在樹狀結構中非常深時,逐層傳遞 props 代價很高,這是一個很好的解決方案。

在這種情況下,我認為這不值得,而採用更簡單的方法會帶來我們想要的結果,只需使用強大的 React.useState()。

使用 React.useState() 鉤子的做法——比使用類別組件簡單得多

如前所述,我們將處理變更並將我們的子組件 fieldEditor 的數據存儲在父組件 fieldForm 中。為此,我們將傳遞一個函數的引用來處理並將更改應用到 fieldForm 狀態,你可以這樣做:

function FieldForm({ fields }) {
  const [fieldsValues, setFieldsValues] = React.useState({});
  const handleChange = (event, fieldId) => {
    let newFields = { ...fieldsValues };
    newFields[fieldId] = event.target.value;

    setFieldsValues(newFields);
  };

  return (
    <div>
      {fields.map(field => (
        <FieldEditor
          key={field}
          id={field}
          handleChange={handleChange}
          value={fieldsValues[field]}
        />
      ))}
      <div>{JSON.stringify(fieldsValues)}</div>
    </div>
  );
}

請注意,React.useState({}) 會返回一個陣列,位置 0 是呼叫時指定的值(在此情況下為空物件),位置 1 是用來修改該值的函數的參考。

現在使用子組件 FieldEditor,你甚至不需要創建帶有 return 語句的函數。一個使用箭頭函數的簡潔常量就可以了!

const FieldEditor = ({ id, value, handleChange }) => (
  <div className="field-editor">
    <input onChange={event => handleChange(event, id)} value={value} />
  </div>
);

好了,我們就結束了,僅此而已。只有這兩個精簡的功能元件,我們的最終目標是「存取」我們的子 FieldEditor 值,並在父節點中展示。

你可以看看五年前的公認答案,看看 Hooks 如何讓 React 程式碼變得更精簡(大幅提升!)。

希望我的回答能幫助你學習並更了解 Hooks,如果你想查看一個可運行的範例,這裡有一個。