【问题标题】:Re-render the component when the store state is changed当 store 状态改变时重新渲染组件
【发布时间】:2020-09-30 04:50:30
【问题描述】:

我被这个问题困住了,我正在使用 redux 来解决这个问题并将问题分为 4 个部分。我想要实现的是将组件道具与另一个组件(也称为 PropEditor 表单)内的 UI 动态映射。我在说什么,首先看到它还没有实现它只是一个我想要实现的原型。

如果您为我提供更好的解决方案来解决此问题,我将不胜感激。

我的做法:

我有一个名为 Heading.js 的组件,其中包含 2 个道具 hasFruit 一个布尔类型和一个 fruitName 字符串类型。它可以是任何库中的组件,但让我们从简单开始。

src/components/Heading.js

import React from 'react';

export const Heading = (props) => {
    const { hasFruit, fruitName } = props;
    return <h1>Fruit name will show { hasFruit ? fruitName : 'Oh no!'}</h1>
};

A 部分:输入类型

我想将此组件道具显示为PropEditor 组件上的 UI。所以,我必须为道具定义不同的 UI 组件。所以,我创建了 2 个输入类型组件。

src/editor/components/types/Boolean.js

import React from 'react';
import PropTypes from 'prop-types';


const propTypes = {
    /** object for the boolean input type. */
    prop: PropTypes.shape({
        /** It will be the name of the prop.  */
        name: PropTypes.string,
        /** It will be the value of the prop.  */
        value: PropTypes.bool,
    }),
    /** onChange handler for the input */
    onChange: PropTypes.func
};

const defaultProps = {
    prop: {},
    onChange: (value) => value,
};



const Boolean = (props) => {

    const { prop, onChange } = props;

    return (
        <input
            id={prop.name}
            name={prop.name}
            type="checkbox"
            onChange={(event) => onChange(event.target.checked)}
            checked={prop.value}
        />
    );

};


Boolean.propTypes = propTypes;
Boolean.defaultProps = defaultProps;

export default Boolean;

src/editor/components/types/Text.js

import React from 'react';
import PropTypes from 'prop-types';

const propTypes = {
    /** object for the text input type. */
    prop: PropTypes.shape({
        /** It will be the name of the prop.  */
        name: PropTypes.string,
        /** It will be the value of the prop.  */
        value: PropTypes.string
    }),
    /** onChange handler for the input */
    onChange: PropTypes.func
};

const defaultProps = {
    prop: {},
    onChange: (value) => value,
};



const Text = (props) => {

    const { prop, onChange } = props;

   const handleChange = (event) => {
        const { value } = event.target;
        onChange(value);
    };


    return (
        <input
            id={prop.name}
            type="text"
            onChange={handleChange}
            value={prop.value}
        />
    );

};


Text.propTypes = propTypes;
Text.defaultProps = defaultProps;

export default Text;

稍后我们将在PropForm 组件中导入这些组件,该组件是PropEditor 组件的子组件。所以我们可以映射这些类型。

src/editor/components/types/index.js

import BooleanType from './Boolean';
import TextType from './Text';

export default {
    boolean: BooleanType,
    text: TextType,
};

B 部分:Redux

整个场景,2个action会派发SET_PROP在store和SET_PROP_VALUE上设置props数据,即当输入改变时通过PropEditor组件派发并更新输入的值。

src/editor/actionTypes:

// PropEditor Actions

// One single prop
export const SET_PROP = 'SET_PROP';

// One single prop value
export const SET_PROP_VALUE = 'SET_PROP_VALUE';

我已经定义了 2 个动作创建者。

src/editor/PropActions.js:

import * as actionTypes from './actionTypes';

// Prop related action creators
/**
 * @param prop {Object} - The prop object
 * @return {{type: {string}, data: {Object}}}
 */
export const setProp = (prop) => {
    return {
        type: actionTypes.SET_PROP,
        data: prop
    };
};


// Prop value related actions
/**
 * @param prop {Object} - The prop object
 * @return {{type: {string}, data: {Object}}}
 */
export const setPropValue = (prop) => {
    return {
        type: actionTypes.SET_PROP_VALUE,
        data: prop
    };
};

src/editor/PropReducer.js:

import * as actionTypes from './actionTypes';

const INITIAL_STATE = {};

export const propReducer = (state = INITIAL_STATE, action) => {
        switch (action.type) {
            // Prop Actions
            case (actionTypes.SET_PROP):
                const { data } = action;
                return { ...state, [data.name]: {...data} };

            // Prop Value Actions
            case (actionTypes.SET_PROP_VALUE):
                return { ...state, [action.data.name]: { ...state[action.data.name], value: action.data.value  } };
            default:
                return state;
        }
};

src/editor/PropStore.js:

import { createStore } from 'redux';
import { propReducer } from './PropReducer';

const REDUX_DEV_TOOL = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();

export const store = createStore(propReducer, REDUX_DEV_TOOL);

使用 DOM 上的 react-redux 提供程序引导我们的整个 App

src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { store } from './editor/PropStore';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

C部分:主要部​​分

如何将组件 Heading.js 的 props 与 PropEditor 组件上的 UI 进行映射?

对于这个用户必须用一个高阶组件来包装它的组件,并且在HOC 内部用户必须调用一些函数,这些函数将在幕后帮助我们动态地填充存储。我创建了一些函数,例如 booleantext,它们将调度一个名为 SET_PROP 的操作来填充存储状态。

src/editor/index.js

import { store } from './PropStore';
import { setProp } from './PropActions';

/**
 * @param name {string} - The name of the prop
 * @param options {Object} - The prop with some additional properties
 * @return {*} - Returns the associated value of the prop
 */
const prop = (name, options)  => {
    const defaultValue = options.value;
    // Create an object and merge with additional properties like `defaultValue`
    const prop = {
        ...options,
        name,
        defaultValue,
    };
    store.dispatch(setProp(prop));
    return defaultValue;
};

/**
 * @param name {string} - The name of the prop
 * @param value {boolean} - The value of the prop
 * @return {boolean} - Returns the value of the prop
 */
export const boolean = (name, value) => {
    // Returns the value of the prop
    return prop(name, { type: 'boolean', value });
};

/**
 * @param name {string} - The name of the prop
 * @param value {string} - The value of the prop
 * @return {text} - Returns the value of the prop
 */
export const text = (name, value) => {
    // Returns the value of the prop
    return prop(name, { type: 'text', value });
};

在 DOM 上渲染 HOC 组件和 PropEditor

src/blocks.js:

import React from 'react';
import { boolean, text } from './editor';
import { Heading } from './components/Heading';


// WithHeading Block
export const WithHeading = () => {
    const boolVal = boolean('hasFruit', true);
    const textVal = text('fruitName', 'Apple');
    return (<Heading hasFruit={boolVal} fruitName={textVal}/>);
};

这是我们的主要App 组件。

src/App.js:

import React from 'react';
import { PropEditor } from './editor/components/PropEditor';
import { WithHeading } from './blocks';

const App = () => {
    return (
        <div className="App">
            {/* PropEditor */}
            <PropEditor />
            {/* Blocks */}
            <WithHeading/>
        </div>
    );
};

export default App;

D 部分:最终部分 PropEditor 组件

PropEditor 将在任何输入发生更改时调度一个动作,但请记住,我们所有的 props 都被转换为用于呈现 UI 的对象数组,这些对象将在 PropForm 组件中传递。

src/editor/components/PropEditor.js:

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { PropForm } from './PropForm';
import { setPropValue } from '../PropActions';

export const PropEditor = () => {

    // Alternative to connect’s mapStateToProps
    const props = useSelector(state => {
        return state;
    });

    // Alternative to connect’s mapDispatchToProps
    // By default, the return value of `useDispatch` is the standard Dispatch type defined by the
    // Redux core types, so no declarations are needed.
    const dispatch = useDispatch();



    const handleChange = (dataFromChild) => {
        dispatch(setPropValue(dataFromChild));

    };

    // Convert objects into array of objects
    const propsArray = Object.keys(props).map(key => {
        return props[key];
    });


    return (
        <div>
            {/* Editor */}
            <div style={styles.editor}>
                <div style={styles.container}>
                    { propsArray.length === 0
                      ? <h1 style={styles.noProps}>No Props</h1>
                      : <PropForm props={propsArray} onFieldChange={handleChange} />
                    }
                </div>
            </div>
        </div>
    );
};

src/editor/components/PropForm.js:

import React from 'react';
import PropTypes from 'prop-types';
import TypeMap from './types';

const propTypes = {
    props: PropTypes.arrayOf(PropTypes.object).isRequired,
    onFieldChange: PropTypes.func.isRequired
};

// InvalidType component
const InvalidType = () => (<span>Invalid Type</span>);

export const PropForm = (properties) => {

    /**
     * @param name {string} - Name of the prop
     * @param type {string} - InputType of the prop
     * @return {Function} - Returns a function
     */
    const makeChangeHandler = (name, type) => {
        const { onFieldChange } = properties;
        return (value = '') => {
            // `change` will be an object and value will be from the onChange
            const change = {name, type, value};
            onFieldChange(change);
        };
    };
    // Take props from the component properties
    const { props } = properties;

    return (
        <form>
            {
                props.map(prop => {
                    const changeHandler = makeChangeHandler(prop.name, prop.type);
                    // Returns a component based on the `type`
                    // if the `type` is boolean then
                    // return Boolean() component
                    let InputType = TypeMap[prop.type] || InvalidType;
                    return (
                        <div style={{marginBottom: '16px'}} key={prop.name}>
                             <label htmlFor={prop.name}>{`${prop.name}`}</label>
                             <InputType prop={prop} onChange={changeHandler}/>
                        </div>
                      );
                })
            }
        </form>
    );
};

PropForm.propTypes = propTypes;

在所有这些解释之后,我的代码运行良好。

问题是当SET_PROP_VALUE 操作在PropEditor 组件内的输入更改上调度时,Heading 组件的重新渲染没有发生。

正如您在 Redux DevTools 扩展中看到的那样,商店已完美更改,但组件 Heading 的重新渲染没有发生。

我认为是因为在我的 HOC text()boolean() 函数内部没有返回更新的值。

有没有办法解决这个问题?

请不要提及这一点,我必须将我的WithHeading 组件与react-redux 连接起来。我知道这一点,但有没有办法像 boolean('hasFruit', true)text('fruitName', 'Apple') 这样的函数在更新商店状态时返回最新值?

代码沙盒:Sandbox

存储库:Repository

【问题讨论】:

  • 如果是parent-child 关系或child-parent 关系,则可以在组件之间进行通信,reactjs 中无法在两个单独的组件之间进行直接通信。您必须使用另一种模式,使用事件发射器模式在PropManager 类上发出事件并在PropEditor 内监听,这是一个复杂的过程。相反,使用 redux 来解决这个问题,创建一个 prop store,并调度一个 action 来操作 prop 值。 redux.js.org/introduction/getting-started
  • @TimmyMucks 你能给我提供一个关于 redux 或 event-emitter-pattern 的例子吗?
  • @TimmyMucks 我编辑了问题,现在使用redux,请再次阅读问题。

标签: reactjs redux react-redux


【解决方案1】:

这里我创建了 4 个演示,每个演示都是前一个演示的扩展版本:

1) 通过 mapStateToProps 连接 sore 和 update 组件

2) 使用useSelector

 const boolVal = useSelector(state => state.hasFruit ? state.hasFruit.value : false );

3) 将动态名称粘贴到 useSelector

const booleanVal = useSelector(state => booleanSelector(state, "hasFruit"));

4)创建了一个自定义钩子,这样你就可以通过名称获取更新的值

const booleanVal = useGetValueFromStore("hasFruit");

问题是标题组件没有重新呈现

原因:

是的,因为它没有连接到商店,它怎么知道store 发生了一些变化,您需要致电connect 与商店建立连接并了解最新的变化。

这是blocks.js的更新代码:

// WithHeading Block
const WithHeading = props => {

  useEffect(() => {
    boolean("hasFruit", true); // <--- Setting initial value
    text("fruitName", "Apple"); // <--- Setting initial value
  }, []); // <----- get called only on mount

  return <Heading hasFruit={props.boolVal} fruitName={props.textVal} />;

};

// to get updated state values inside the component as props
const mapStateToProps = state => {
  return {
    boolVal: state.hasFruit ? state.hasFruit.value : false,
    textVal: state.fruitName ? state.fruitName.value : ""
  };
};

// to make connection with store
export default connect(mapStateToProps)(WithHeading);

1) 工作演示


另一种方法是您可以使用useSelector

// WithHeading Block
const WithHeading = props => {
  // console.log(props);
  const boolVal = useSelector(state =>
    state.hasFruit ? state.hasFruit.value : false
  );
  const textVal = useSelector(state =>
    state.fruitName ? state.fruitName.value : ""
  );

  useEffect(() => {
    boolean("hasFruit", true);
    text("fruitName", "Apple");
  }, []);

  return <Heading hasFruit={boolVal} fruitName={textVal} />;
};

export default WithHeading;

2) 工作演示:

您也可以将选择器放在单独的文件中,以便您可以随时使用它

const WithHeading = props => {
  // you can pass the input names here, and get value of it
  const booleanVal = useSelector(state => booleanSelector(state, "hasFruit"));
  const textVal = useSelector(state => textValSelector(state, "fruitName"));

  useEffect(() => {
    boolean("hasFruit", true);
    text("fruitName", "Apple");
  }, []);

  return <Heading hasFruit={booleanVal} fruitName={textVal} />;
};

3) 工作演示:

使用useSelector 的自定义挂钩:

// a function that will return updated value of given name
const useGetValueFromStore = name => {
  const value = useSelector(state => (state[name] ? state[name].value : ""));
  return value;
};

// WithHeading Block
const WithHeading = props => {

  //------- all you need is just to pass the name --------
  const booleanVal = useGetValueFromStore("hasFruit");
  const textVal = useGetValueFromStore("fruitName");

  useEffect(() => {
    boolean("hasFruit", true);
    text("fruitName", "Apple");
  }, []);

  return <Heading hasFruit={booleanVal} fruitName={textVal} />;
};

export default WithHeading;

4) 工作演示:

【讨论】:

  • 我知道这一点,但是当存储状态更新时,boolean('hasFruit', true)text('fruitName', 'Apple') 之类的函数是否可以返回最新值?
  • @VenNilson, Is there a way the functions like boolean('hasFruit', true) and text('fruitName', 'Apple') ,我已经更新了答案,我认为最后一个演示将为您的问题提供答案。
  • 感谢您的回答。创建自定义钩子是个好主意,我注意到了这一点。
  • @VenNilson,很高兴知道,它有帮助,:)
  • 我当然会,但 StackOverflow 说你可以在 24 小时后奖励你的赏金。
【解决方案2】:

在 React 中有多种处理状态的方法,其中许多选择基于复杂性和需求。如 cmets 中所述,Redux 是一个强大的选项。 Mobx 是一项了不起的技术,仅举两例。

React 本身确实有能力在没有外部库的情况下传播和响应这些变化。您可以考虑使用Context API -

./src/contexts/Store

import React, {
  useContext,
  useState,
  useMemo,
  createContext,
  useEffect,
} from 'react';


const StoreContext = createContext(null);

const StoreProvider = (props) => {
  const [state, setLocalState] = useState({});

  function set(objToMerge) {
    setLocalState({ ...state, ...objToMerge });
  }

  function get(k) {
    return state[k];
  }

  function getAll(){
    return state;
  }

  const api = useMemo(() => {get, set, getAll}, []);
  return <StoreContext.Provider value={api} {...props}></StoreContext.Provider>;
};

function useStoreContext(): StoreProviderApi {
  const api = useContext(StoreContext);
  if (api === null) {
    throw new Error(
      'Component must be wrapped in Provider in order to access API',
    );
  }
  return api;
}

export { StoreProvider, useStoreContext };

要使用,您确实需要一个父级组件 -

import {StoreProvider} from './contexts/Store';

...
    <StoreProvider>
      <PropEditor/>
      <WithHeading/>
    </StoreProvider>
...

然后,在组件本身内,您可以访问最新状态 -

import {useStoreContext} from './contexts/Store';

export const Heading = (props) => {
    const store = useStoreContext();

    const { hasFruit, fruitName } = store.getAll();
    return <h1>Fruit name will show { hasFruit ? fruitName : 'Oh no!'}</h1>
};

这样做的好处是不需要传递大量道具,并且会在更改时自动渲染。

然而,缺点是它在更改时重新渲染。也就是说,没有任何机制可以选择性地仅重新渲染具有更改的 props 的组件。许多项目有多种环境来缓解这种情况。

如果你的 store props 需要在整个应用程序中使用,那么 Redux (with the toolkit) 是一个不错的选择,因为它是 React 之外的 store,它只处理将 props 更改广播到订阅组件的那些props,而不是重新渲染所有订阅者(这是 Context API 所做的)。

到那时,它就变成了架构问题以及您的应用程序需求需要什么。

【讨论】:

  • 我更新了问题现在很容易回答问题。
猜你喜欢
  • 2019-08-08
  • 2014-09-30
  • 2020-04-22
  • 1970-01-01
  • 2019-06-13
  • 2020-01-20
  • 2021-10-28
  • 2020-12-30
  • 2019-09-21
相关资源
最近更新 更多