15 votes

Rendu du composant lorsque l'état du magasin est modifié

Je suis bloqué sur ce problème, j'utilise redux pour résoudre ce problème et j'ai divisé la question en 4 parties. Ce que j'essaie de faire est de mapper dynamiquement les props d'un composant avec l'interface utilisateur d'un autre composant (aussi connu sous le nom de PropEditor Form). Ce que je veux dire, c'est que ce n'est pas encore implémenté, c'est juste un prototype que je veux implémenter.

PropEditor Prototype

J'apprécierais également que vous me fournissiez une meilleure solution pour résoudre ce problème.

Mon approche :

J'ai un composant nommé Heading.js qui contient 2 accessoires hasFruit un type booléen et un fruitName type de chaîne. Il peut s'agir d'un composant de n'importe quelle bibliothèque, mais commençons par le plus simple.

src/composants/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>
};

Partie A : Types d'entrée

Je souhaite afficher ce composant en tant qu'interface utilisateur sur la page PropEditor de la composante. Je dois donc définir les différents composants de l'interface utilisateur pour les accessoires. J'ai donc créé 2 composants de type input.

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;

Plus tard, nous importerons ces composants dans PropForm qui est l'enfant du composant PropEditor de la composante. Nous pouvons donc mettre en correspondance ces types.

src/editor/components/types/index.js

import BooleanType from './Boolean';
import TextType from './Text';

export default {
    boolean: BooleanType,
    text: TextType,
};

Partie B : Redux

Tout au long du scénario, 2 actions seront menées SET_PROP pour définir les données de l'accessoire dans le magasin et SET_PROP_VALUE c'est-à-dire l'envoi par PropEditor lorsque l'entrée est modifiée et met à jour la valeur de l'entrée.

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';

J'ai défini deux créateurs d'actions.

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 de l'ensemble de notre App avec le react-redux sur le 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')
);

Partie C : Partie principale

Comment cartographier un composant Heading.js avec une interface utilisateur sur le PropEditor composant ?

Pour ce faire, l'utilisateur doit envelopper son composant dans un composant d'ordre supérieur et à l'intérieur de celui-ci HOC L'utilisateur doit appeler certaines fonctions qui, dans les coulisses, nous aideront à remplir dynamiquement le magasin. J'ai créé quelques fonctions comme boolean y text qui enverra une action nommée SET_PROP pour remplir l'état du magasin.

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 });
};

Rendre le HOC et PropEditor sur le 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}/>);
};

Il s'agit de notre principal App de la composante.

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;

Partie D : Partie finale PropEditor composante

PropEditor enverra une action lorsqu'une entrée est modifiée, mais n'oubliez pas que toutes nos actions de props sont convertis en un tableau d'objets pour le rendu de l'interface utilisateur, qui sera passé à l'intérieur de la fonction PropForm de la composante.

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;

Après toutes ces explications, mon code fonctionne parfaitement.

Le problème est le re-rendement de l'image Heading ne se produit pas lorsque le composant SET_PROP_VALUE est déclenchée lors de la modification de l'entrée à l'intérieur de l'élément PropEditor de la composante.

Redux Store Debugging

Le magasin est parfaitement modifié, comme vous pouvez le voir avec le bouton Outils de développement Redux mais le re-rendu du composant Heading n'a pas lieu.

Je pense que c'est parce qu'à l'intérieur de mon HOC text() y boolean() ne renvoient pas de valeur actualisée.

Existe-t-il un moyen de résoudre ce problème ?

S'il vous plaît, ne le mentionnez pas Je dois connecter mon WithHeading avec le composant react-redux . Je sais cela, mais existe-t-il un moyen pour que les fonctions telles que boolean('hasFruit', true) y text('fruitName', 'Apple') renvoie la dernière valeur lorsque l'état du magasin est mis à jour ?

Codesandbox : Bac à sable

Dépôt : Référentiel

3voto

Vivek Doshi Points 18852

Ici, j'ai créé 4 démos, chaque démo est une version étendue de la précédente :

1) Connecter le sore et mettre à jour le composant via mapStateToProps

2) En utilisant le useSelector

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

3) Paasage du nom dynamique à utiliserSelector

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

4) Création d'un crochet personnalisé, pour que vous puissiez obtenir la valeur mise à jour en passant simplement le nom.

const booleanVal = useGetValueFromStore("hasFruit");

Le problème est que le re-rendement du composant Heading ne se produit pas.

Raison :

Oui, parce qu'il n'est pas connecté au magasin, comment peut-il savoir qu'il y a des changements en cours ? store Vous devez appeler connect pour établir un lien avec le magasin et être au courant des changements.

Voici le code mis à jour du 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) DÉMONSTRATION DE TRAVAIL :

Edit #SO-redux-connect


Une autre approche consiste à utiliser 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) DÉMONSTRATION DE TRAVAIL :

Edit #SO-redux-connect2

Vous pouvez également placer le sélecteur dans un fichier séparé, afin de pouvoir l'utiliser à tout moment.

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) DÉMONSTRATION DE TRAVAIL :

Edit #SO-redux-connect3

Crochet personnalisé avec l'utilisation de 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) DÉMONSTRATION DE TRAVAIL :

Edit #SO-redux-connect4

1voto

nrako Points 952

Il y a plusieurs façons de gérer l'état dans React, et beaucoup de ces choix sont basés sur la complexité et les besoins. Comme mentionné dans les commentaires, Redux est une option puissante. Mobx est une technologie remarquable, pour n'en citer que deux.

React lui-même a la capacité de diffuser et de répondre à ces changements sans librairie externe. Vous pouvez envisager d'utiliser la bibliothèque API contextuelle -

./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 };

pour l'utiliser, vous avez besoin d'un composant de niveau Parent -

import {StoreProvider} from './contexts/Store';

...
    <StoreProvider>
      <PropEditor/>
      <WithHeading/>
    </StoreProvider>
...

Ensuite, dans le composant lui-même, vous pouvez accéder au dernier état -

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>
};

L'avantage est qu'il n'est pas nécessaire de passer des tonnes d'accessoires, et que le rendu se fait automatiquement en cas de changement.

L'inconvénient, cependant, est qu'il volonté le re-rendu en cas de changement. En d'autres termes, il n'existe pas de mécanisme permettant de rendre sélectivement les composants dont les accessoires ont été modifiés. De nombreux projets ont plusieurs contextes pour pallier ce problème.

Si les accessoires de votre magasin doivent être utilisés dans l'ensemble de l'application, Redux ( avec la boîte à outils ) est une bonne option, parce que c'est un magasin en dehors de React, et qu'il ne diffuse que les changements de prop aux composants abonnés pour ces props, plutôt que de re-rendre tous les abonnés (ce que fait l'API Contexte).

À ce stade, il s'agit d'une question d'architecture et de ce qui est nécessaire pour répondre aux exigences de votre application.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X