I'm stuck on this problem, I am using redux to solve this problem and divided question into 4 parts. What I am trying to achieve is dynamically map component props with the UI inside another component (also known as PropEditor Form). What I'm talking about, First see this it is not implemented yet it just a prototype that I want to implement.
I will also appreciate it if you provide me a better solution to solve this problem.
My approach:
I have a component named Heading.js which contains 2 props hasFruit a boolean type and a fruitName string type. It can be a component from any library but let's start with simple.
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>
};
Part A: InputTypes
I want to show this component props as a UI on the PropEditor component. So, I have to define the different UI components for the props. So, I have created 2 input type components.
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;
Later we will import these components inside PropForm component which is the child of the PropEditor component. So we can map these types.
src/editor/components/types/index.js
import BooleanType from './Boolean';
import TextType from './Text';
export default {
boolean: BooleanType,
text: TextType,
};
Part B: Redux
The whole scenario, 2 actions will dispatch SET_PROP to set prop data on the store and SET_PROP_VALUE i.e. dispatch through PropEditor component when the input is changed and its update the value of the input.
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';
I have defined 2 action creators.
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);
Bootstrap our whole App with the react-redux provider on the DOM.
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')
);
Part C: Main part
How to map component Heading.js props with a UI on the PropEditor component?
For this user has to wrap its component with a higher-order component and inside that HOC user has to call some functions which will behind the scenes help us to dynamically populate the store. I have created some functions like boolean and text which will dispatch an action named SET_PROP to populate the store state.
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 });
};
Render the HOC component and PropEditor on the DOM:
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}/>);
};
This is our main App component.
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;
Part D: Final Part PropEditor component
PropEditor will dispatch an action when any input is changed but remember all our props are converted into an array of objects for rendering the UI which will be passed inside the PropForm component.
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;
After all this explanation my code is working perfectly.
The problem is re-rendering of the Heading component is not happening when SET_PROP_VALUE action is dispatched on the input change inside the PropEditor component.
The store is changed perfectly as you can see with the Redux DevTools extension but the re-render of the component Heading is not happening.
I think because inside my HOC text() and boolean() functions are not returning an updated value.
Is there a way to solve this problem?
Please don't mention this I have to connect my WithHeading component with the react-redux. I know this but Is there a way the functions like boolean('hasFruit', true) and text('fruitName', 'Apple') returns the latest value when the store state is updated?
Codesandbox: Sandbox
Repository: Repository

