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
React components automatically re-render whenever there is a change in their state or props. A simple update of the state, from anywhere in the code, causes all the User Interface (UI) elements to be re-rendered automatically.
1. Memoization using useMemo() and UseCallback() Hooks. Memoization enables your code to re-render components only if there's a change in the props. With this technique, developers can avoid unnecessary renderings and reduce the computational load in applications.
React-redux component does not rerender on store state change.
Here I've created 4 demos, each demo is an extended version of the previous one :
1) Connect the sore and update component via mapStateToProps
2) By Using the useSelector
const boolVal = useSelector(state => state.hasFruit ? state.hasFruit.value : false );
3) Paasing the dynamic name to useSelector
const booleanVal = useSelector(state => booleanSelector(state, "hasFruit"));
4) Created a custom hook, so that you can get the updated value bu just passing the name
const booleanVal = useGetValueFromStore("hasFruit");
The problem is re-rendering of the Heading component is not happening
Reason :
Yes because it's not connected to the store, how does it know that there are some changes going on the store
, you need to call connect
to make a connection with the store and be up to date with changes.
Here is the updated code of the 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) WORKING DEMO :
Another approach is you can use 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) WORKING DEMO :
You can also put the selector in separate file,so that you can use it whenever you want
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) WORKING DEMO :
Custom Hook with use of 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) WORKING DEMO :
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With