我一直在解决这个问题,我正在使用Redux来解决此问题,并将问题分为4部分。我要实现的目标是将UI与另一个组件(也称为PropEditor Form)内的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部分: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
用户必须在内部调用一些函数,这些函数将在后台帮助我们动态填充商店。我创建了一些函数,例如boolean
和text
,这些函数将调度名为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 DevTools扩展名可以很好地更改存储,但是组件Heading
的重新呈现并未发生。
我认为是因为在我的HOC
text()
和boolean()
函数内部未返回更新后的值。
是否可以解决此问题?
请不要提及这一点,我必须将WithHeading
组件与react-redux
连接起来。我知道这一点,但是有没有办法在存储状态更新时像boolean('hasFruit', true)
和text('fruitName', 'Apple')
这样的函数返回最新值?
Codesandbox:Sandbox
存储库:Repository
答案 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)工作演示:
另一种方法是您可以使用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)工作示例:
答案 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的作用)。
到那时,这成为体系结构以及您的应用程序需求需要什么的问题。