Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Re-render the component when the store state is changed

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.

PropEditor Prototype

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.

Redux Store Debugging

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

like image 307
Ven Nilson Avatar asked Jun 10 '20 17:06

Ven Nilson


People also ask

Do components re-render when state changes?

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.

How do you prevent components from rendering after state change?

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.

Does Redux state change cause re-render?

React-redux component does not rerender on store state change.


1 Answers

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 :

Edit #SO-redux-connect


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 :

Edit #SO-redux-connect2

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 :

Edit #SO-redux-connect3

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 :

Edit #SO-redux-connect4

like image 76
Vivek Doshi Avatar answered Oct 03 '22 22:10

Vivek Doshi