更改存储状态后,重新渲染组件

时间:2020-06-10 17:07:35

标签: reactjs redux react-redux

我一直在解决这个问题,我正在使用Redux来解决此问题,并将问题分为4部分。我要实现的目标是将UI与另一个组件(也称为PropEditor Form)内的UI动态映射。我要说的是,首先看到它尚未实现,而只是我要实现的原型。

PropEditor Prototype

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

我的方法:

我有一个名为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部分:InputTypes

我想在PropEditor组件上以UI形式显示该组件props。因此,我必须为道具定义不同的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个动作将分派SET_PROP来在商店上设置道具数据,并且SET_PROP_VALUE会在输入改变并更新其值时通过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上的App提供程序引导整个react-redux

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组件上的UI映射组件PropEditor道具?

因为该用户必须用一个高阶组件包装其组件,并且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;

经过所有这些解释,我的代码运行良好。

问题在于,当对Heading组件内的输入更改分派SET_PROP_VALUE动作时,PropEditor组件不会重新呈现。

Redux Store Debugging

使用Redux DevTools扩展名可以很好地更改存储,但是组件Heading的重新呈现并未发生。

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

是否可以解决此问题?

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

Codesandbox:Sandbox

存储库:Repository

2 个答案:

答案 0 :(得分:3)

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

1)通过mapStateToProps连接痛处并更新组件

2)通过使用useSelector

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

3):将动态名称设置为useSelector

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

4)创建了一个自定义挂钩,以便您只需传递名称即可获取更新后的值bu

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)工作演示

Edit #SO-redux-connect


另一种方法是您可以使用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)工作示例:

Edit #SO-redux-connect2

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

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)工作示例:

Edit #SO-redux-connect3

使用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)工作示例:

Edit #SO-redux-connect4

答案 1 :(得分:1)

在React中有几种处理状态的方法,其中许多选择都是基于复杂性和需求。如评论中所述,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>
};

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

但是,缺点是它会在更改时重新呈现。也就是说,没有机制可以选择性地仅重新渲染具有更改道具的组件。许多项目具有多种环境来减轻这种情况。

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

到那时,这成为体系结构以及您的应用程序需求需要什么的问题。