Redux: Je donne initialState dans createStore, puis combineReducers affiche l'un de mes réducteurs renvoyé non défini lors de l'initialisation

Créé le 23 août 2017  ·  27Commentaires  ·  Source: reduxjs/redux

en réducteurs :

const todoBlog = combineReducers({
  blogTypeVisibilityFilter,
  blogs
})

dans blogTypeVisibilityFilter :

const blogTypeVisibilityFilter = (state, action)=>{
  switch (action.type) {
    case 'BLOG_TYPE_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

export default blogTypeVisibilityFilter;

dans les blogs :

const blogs = (state,action)=>{
  return state
}

dans createStore :

const initialState = {
  blogTypeVisibilityFilter:'SHOW_ALL_BLOG',
  blogs:data.data,
}

const store = createStore(reducer,initialState,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());

et puis ça s'affiche mal :

Le réducteur "blogTypeVisibilityFilter" est retourné indéfini lors de l'initialisation. Si l'état passé au réducteur n'est pas défini, vous devez explicitement renvoyer l'état initial. L'état initial ne peut pas être indéfini. Si vous ne souhaitez pas définir de valeur pour ce réducteur, vous pouvez utiliser null au lieu de undefined.

mais quand je change juste

const todoBlog = combineReducers({
  blogTypeVisibilityFilter,
  blogs
})

à

const todoBlog = (state={},action)=>{
  return{
    blogTypeVisibilityFilter:blogTypeVisibilityFilter(state.blogTypeVisibilityFilter,action),
    blogs:blogs(state.blogs,action)
  }
}

dans les réducteurs cela fonctionne bien et sans aucune erreur

pourquoi j'utilise combinerReducers ça se passe mal ?

Commentaire le plus utile

Ceci est un traqueur de bogues, pas un système de support. Pour les questions d'utilisation, veuillez utiliser Stack Overflow ou Reactiflux. Merci!

Tous les 27 commentaires

Ceci est un traqueur de bogues, pas un système de support. Pour les questions d'utilisation, veuillez utiliser Stack Overflow ou Reactiflux. Merci!

J'ai le même problème.

Pas:

  1. Image en forme de magasin simple { foo, bar } .
  2. Créez un réducteur simple comme simpleReducer = state => state .
  3. Combinez les réducteurs en reducers = combineReducers({ foo: simpleReducer, bar: simpleReducer }) .
  4. Créez l'état avec la valeur initiale avec createState(reducers, { foo: Obj1, bar: Obj2 }) .
  5. Obtenir l'erreur Reducer "foo" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state

La raison est assertReducerShape qui exécute tous les réducteurs avec un état indéfini pour obtenir les parties de l'état initial. Mon réducteur simple renvoie son argument qui n'est pas défini dans ce cas. Pourquoi l'appel Obj1 de createState n'est pas considéré comme l'état initial de foo-part ?

@timdorr pouvez-vous rouvrir le problème ?

La solution de contournement consiste à créer simpleReducer = (state = null) => state . Mais est-ce que ça va ?

Avec combineReducers , chaque réducteur de tranche devrait "posséder" sa tranche d'état. Cela signifie fournir une valeur initiale au cas où la tranche actuelle n'est pas définie, et combineReducers indiquera si vous ne le faites pas.

« État initial » a plusieurs significations. Il s'agit de la valeur de retour par défaut de reducer et du deuxième argument de createStore. Je pense qu'il y a un problème. Pouvez-vous expliquer pourquoi combineReducers vérifie les réducteurs pour initialState et createState pas ?

@Vittly : combineReducers est délibérément opiniâtre, alors que createStore ne l'est pas. createStore appelle simplement la fonction de combineReducers suppose que si vous l'utilisez, vous souscrivez à un ensemble spécifique d'hypothèses sur la façon dont l'état doit être organisé et comment les réducteurs combinés doivent se comporter.

Vous voudrez peut-être lire les n ° 191 et n ° 1189, qui expliquent pourquoi combineReducers est opiniâtre et quelles sont ses opinions.

Le problème est donc (bientôt) "si vous combinez des réducteurs, vous divisez également votre arbre de magasin et vous pouvez manquer quelque chose ou précharger délibérément une partie du magasin dans createStore 2nd arg. Pour rendre l'arbre de magasin plus cohérent, utilisez assertReducerShape". D'accord, merci pour les références

Même problème ici.
J'utilise donc createStore et passe un état initial. Mais dans le fichier reduct.js , la fonction assertReducerShape redux vérifiera mes réducteurs lors du passage d'un état undefined .

IMHO c'est un bug.

@JoseFMP : Je garantirais effectivement que tout ce que vous voyez n'est _pas_ un bogue. Cependant, si vous pouvez créer un référentiel ou CodeSandbox qui démontre le problème, nous pouvons y jeter un coup d'œil.

Sûr. J'ai déposé un problème dans Stackoverflow :
https://stackoverflow.com/questions/53018766/why-redux-reducer-getting-undefined-instead-of-the-initial-state

Le problème est assez simple. Donc, si je définis un état initial lors de la création du magasin... pourquoi redux voudrait-il évaluer le réducteur avec un état undefined ? Cela fait en effet que tous les réducteurs retournent un nouvel état undefined .

Mais c'est un non-sens, puisque nous passons un état initial.
Donc c'est raté :

import { combineReducers, createStore } from 'redux';

const configReducer = (config: any, action: any): any =>{
        return config;
}

const customData = (customData: any, action: any): any =>  {
        return customData;
}
const reducers = combineReducers({config: configReducer, customData: customDataReducer})

const defaultConfig = "cool config";
const data = "yieaah some data";

var initialState = {config: defaultConfig, customData: data};
const store = createStore(reducers, initialState) // at this point redux calls all the reducers with 'undefined' state. Why?

@JoseFMP :

Ah, je pense que je sais quel est le problème. Ceci est spécifique au fonctionnement de combineReducers() .

combineReducers s'attend state === undefined , il renverra une valeur par défaut appropriée. Pour vérifier cela, combineReducers() appellera en fait votre réducteur avec (undefined, {type : "SOME_RANDOMIZED_ACTION_TYPE"}) pour voir s'il renvoie une valeur indéfinie ou "réelle".

Vos réducteurs ne font actuellement rien d'autre que retourner tout ce qui est passé. Cela signifie que si state n'est pas défini, ils _retourneront_ indéfinis. Ainsi, combineReducers() vous dit que vous cassez le résultat attendu.

Modifiez simplement les déclarations en quelque chose comme configReducer = (config = {}, action) , etc, et cela corrigera les avertissements.

Encore une fois, pour être clair : ce n'est _pas_ un bogue. C'est la validation du comportement.

Hej @markerikson merci pour ta réponse.
Quelques commentaires :

1) Non, ce n'est PAS spécifique de combineReducers . Si je n'utilise pas combineReducers et implémente mon propre réducteur de racine, la même chose se produit.

2) L'incohérence ici est que, dans la documentation Redux, il est mentionné que si le réducteur est appelé avec un statut inconnu ou inattendu, il ne doit pas modifier le statut et renvoyer la valeur fournie en argument. C'est-à-dire ... s'il reçoit undefined il devrait alors retourner undefined . Mais une autre règle dit que le réducteur ne doit jamais retourner undefined donc ces deux règles, telles qu'elles sont actuellement dans la documentation de redux, sont incohérentes. Et incompatible avec la mise en œuvre.

@JoseFMP : comme je l'ai dit précédemment, si vous pouvez fournir un CodeSandbox qui démontre spécifiquement le problème, avec des commentaires indiquant exactement ce que vous attendez de se produire par rapport à ce qui se passe réellement, je peux peut-être y jeter un œil. (En outre, veuillez indiquer les sections de documentation que vous jugez incohérentes.) Jusque-là, je ne peux que mettre cela sur le compte d'un malentendu sur le fonctionnement interne de Redux.

Merci @markerikson .
À propos de la documentation :
image

Donc, si le réducteur est appelé avec une action inconnue, je devrais retourner le même état car le réducteur ne sait pas ce que l'action doit faire dans ce réducteur.

Lors de la création du magasin, Redux vérifie les réducteurs en leur envoyant une action inconnue et un état précédent undefined . Donc, si l'état précédent était undefined le réducteur devrait retourner undefined selon la documentation (car le réducteur ne connaît pas l'action). Mais si le réducteur renvoie undefined , je vous garantis qu'aucune application Redux ne pourrait fonctionner.

Pour l'exemple de code :

import { createStore } from 'redux';

const configReducer = (config: any, action: any): any =>{
        return config;
}

const customReducer = (customData: any, action: any): any =>  {
        return customData;
}

const reducers = (currentState: IAppState, action: any): IAppState => {

    var appStateToEvaluate: any;
    if (currentState) { //needs to add this to pass the `undefined` check of redux
        appStateToEvaluate = currentState;
    }
    else{
        //why redux is doing this ?!
        appStateToEvaluate = {}
    }
    const newState: IAppState = {
        cvConfig: configReducer(appStateToEvaluate.config, action),
        personalData: customReducer(appStateToEvaluate.customData, action)
    }

    return newState;
}

const defaultConfig = "cool config";
const data = "yieaah some data";

var initialState = {config: defaultConfig, customData: data};
const store = createStore(reducers, initialState) // at this point redux calls all the reducers with 'undefined' state. Why?

@JoseFMP : Je pense que la principale différence qui vous manque ici est que dans cet exemple, la fonction de réduction a _déjà_ géré le cas où state est undefined , en utilisant la syntaxe des arguments par défaut ES6 :

function todoApp(state = initialState, action) {

Le conseil du didacticiel est donc correct - un réducteur _devrait_ toujours renvoyer l'état existant, _en supposant que le cas non défini a déjà été traité_.

@markerikson
Merci pour votre réponse.
C'est clair pour moi. Manque juste dans le tuto. Dans le tutoriel, il n'est pas dit que le cas à retourner est celui spécifié comme valeur de paramètre par défaut dans le réducteur. Donc la phrase que vous avez mise en italique est correcte. C'est ça. Mon souci est qu'il manque dans le tutoriel. Ou je ne l'ai pas trouvé, ou est ambigu.

Maintenant, indépendamment du tutoriel et/ou de la documentation, je trouve personnellement que cette vérification undefined n'a pas de sens pour le cas spécifique dans lequel un état initial est spécifié. Si aucun état initial n'est spécifié, je trouve que c'est ok. Maintenant ce n'est pas une discussion, juste mon avis : si un état initial est précisé je trouve inutile de faire cette vérification. Mais je peux (et dois) vivre avec quand même ;)

Merci pour votre soutien Marc.

Nous pouvons certainement essayer de reformuler une partie du phrasé dans le cadre de notre refonte plus large de la documentation (#2590).

Cela dit, l'une des idées originales de Redux était que chaque "réducteur de tranches" est responsable de "posséder" sa partie de l'état, qui inclut à la fois les mises à jour et la fourniture d'une valeur initiale. Vous semblez être un peu coincé sur l'aspect "Eh bien, je fournis une valeur initiale à createStore ", mais vous écartez les attentes quant à la façon dont Redux s'attend à ce qu'il se comporte même si vous ne le faites pas. fournir la valeur séparément.

Quelque peu surpris que je n'aie pas encore fait le lien, mais vous voudrez peut-être lire la page Initializing State dans la documentation.

@markerikson
Oui tu as raison. Je connais déjà la documentation sur l'état initial. Ce que je veux dire (et je crois que c'est la raison pour laquelle ce problème a été créé et les gens ont également voulu dire dans cette direction), c'est qu'il est contre-intuitif de fournir "un état initial" lors de la création du magasin et de toujours définir un "état par défaut" dans les réducteurs de tranche (ou réducteur de racine). C'est-à-dire parce que très souvent, mais pas nécessairement, ils sont identiques, ou très liés, il semble contre-intuitif de devoir les définir deux fois. Voir comme exemple les publications de @ElonXun ou @Vittly qui se sont confondus comme moi. C'est-à-dire que ma remarque n'est pas l'API de Redux, c'est à quel point il est intuitif d'utiliser l'API de Redux dans ce scénario particulier, d'un point de vue purement humain.

Notez que le dernier paragraphe concerne le ressenti humain lors de l'utilisation de l'API de redux. La mise en œuvre de la machine ou les raisons qui la sous-tendent peuvent être tout à fait légitimes. Mais en tant que consommateur d'API, cela semble déroutant.

Par exemple, pour moi, lors du développement, j'ai très souvent un état initial dans mon application. Donc, généralement, je dois le taper deux fois. Une fois pour le brancher lorsque je crée le magasin et une autre fois pour le distribuer comme valeur par défaut dans les réducteurs de tranche. Bien sûr de nombreuses solutions pour cela. Mais le principe qui pour moi, en tant qu'humain, rend les choses déroutantes, c'est que je dois taper deux fois la même chose.

Cependant, je pense qu'avoir un cas particulier dans lequel l'état initial est défini et ne pas rendre obligatoire que les réducteurs de tranche ou le réducteur racine aient un "état par défaut" est plus problématique que de le rendre obligatoire.

Donc, la seule contribution ici est de mentionner que cela semble un peu contre-intuitif. Mais juste ça.

Je ne me souviens pas avoir appelé combineReducers assertReducerShape dans les versions précédentes. Dans mon code undefined est un état invalide de mes réducteurs. Je n'ai pas besoin de combinerReducers pour m'en assurer, j'en ai besoin pour "combiner les réducteurs" et recréer l'objet racine si quelque chose change. Cela va un peu au-delà de ce à quoi je m'attendrais. OMI, c'est trop opiniâtre maintenant.

Certes, je n'ai pas utilisé combinerReducers depuis un certain temps car je n'en ai pas eu besoin dans l'application de base que j'ai développée. Mais j'essaie maintenant d'encapsuler cette application, et j'ai pensé que combinerReducers était bien à utiliser pour découper l'application. Le comportement de assertReducerShape est surprenant.

@lukescott : ce chèque est là depuis septembre 2015 :

https://github.com/reduxjs/redux/commit/a1485f0e30ea0ea5e023a6d0f5947bd56edff7dd

Et oui, combineReducers() est _délibérément_ opiniâtre. Si vous ne voulez pas ces avertissements, il est assez facile d'écrire votre propre fonction similaire sans ces vérifications.

Comme exemple spécifique, voir l'essentiel de Dan Redux en 100 lignes sans les contrôles .

Je comprends. L'ancienne version passait l'état initial et vérifiait l'absence de définition à chaque exécution (si je me souviens bien). Ce qui est surprenant, c'est que l'état initial n'est pas transmis au premier passage. Passer undefined est une erreur dans mon application. Comme je l'ai dit, je travaille sur ce projet depuis un certain temps et je n'ai pas utilisé combinerReducers depuis un certain temps. Je viens juste de recommencer à l'utiliser, en plaçant notre application dans un wrapper.

Je comprends aussi que c'est opiniâtre. Mais cette opinion était "un réducteur ne doit pas revenir indéfini" - qui est la règle que je respecte. Il est devenu "nous allons passer indéfini et vous devez créer l'état vous-même à partir de rien". combineReducers ne fonctionne plus avec "il y a toujours un état initial" - ce qui est regrettable. Cela change radicalement les règles qui étaient en place il y a 3 ans.

Je ne sais vraiment pas à quels changements de comportement vous faites référence. Pouvez-vous citer des exemples précis ?

La page Initializing State docs présente les interactions entre l'argument preloadedState pour createStore , la gestion de state = initialState pour un réducteur et combineReducers() . C'est basé sur une réponse de Stack Overflow que Dan a écrite au début de 2016, et à ma connaissance, rien de significatif n'a changé à cet égard.

@markerikson Vous avez raison. J'ai regardé en arrière jusqu'à 2.0, et il semble qu'il ait toujours fait cela. Peut-être que la complexité de ma demande vient de la rendre plus apparente.

Le problème que j'ai est que mon réducteur est défini comme:

reducer(state: State, action: Action)

Cela signifie que cet état ne doit pas être indéfini. Cela signifie qu'il doit y avoir un état initial. Mais parce que combineReducers appelle reducer(undefined, INIT) pour le vérifier, cela provoque une erreur dans mon code (s'il réussit, il appelle plus tard reducer(initState, INIT) - appelant INIT deux fois).

Cela signifie que tout réducteur utilisé dans combineReducers DOIT être défini comme :

reducer(state: State | undefined, action: Action)

Donc, mon affirmation selon laquelle il ne l'a pas fait avant est incorrecte. Mais le problème que j'ai, et celui de l'OP, est probablement le même : cela vous oblige à déclarer vos réducteurs avec un état facultatif. Je ne reçois pas réellement les avertissements, cela fait planter mon code car il attend un état non défini.

Si vous dites que ça doit être comme ça et que je dois le rouler moi-même, c'est bien. C'est juste dommage car en plus d'affirmer la forme, il vérifie déjà undefined au moment de l'exécution. Affirmer la forme semble un peu exagéré et contre-productif.

Oui. Comme décrit dans cette page de documentation, lorsque vous utilisez combineReducers , l'on s'attend spécifiquement à ce que chaque réducteur de tranche soit responsable de renvoyer son propre état initial, et il devrait le faire en réponse à sliceReducer(undefined, action) . Du point de combineReducers , votre code _is_ buggy car il n'adhère pas au contrat attendu, et donc il vous dit que le code est faux.

Ne fournissez-vous pas réellement un état initial ? A quoi ressemble réellement le code ?

Je fournis un état initial via createStore. Mais cet état initial n'est pas transmis à mon réducteur car assertReducerShape appelle explicitement mon réducteur avec undefined , malgré le passage d'un état initial :

https://github.com/reduxjs/redux/blob/231f0b32641059caab3f98a3e04d3afaad19a7d1/src/combineReducers.js#L68

Mon code n'est pas buggé. Cela nécessite juste que undefined ne lui soit _jamais_ passé. Un état initial est requis et est généré à partir du serveur. combineReducers rompt simplement ce contrat et rend impossible la saisie stricte du réducteur avec un état requis. Je peux le faire sans combinerReducers et cela fonctionne très bien. Je suppose que c'est ce que je devrai faire - mais IMO - forcer un état facultatif n'est pas souhaitable et, dans mon cas, rend combineReducers inutile car cela casse mon application strictement typée.

Ce que je ne comprends pas, c'est pourquoi assertReducerShape est nécessaire. Il vérifie déjà undefined ici au moment de l'exécution :

https://github.com/reduxjs/redux/blob/231f0b32641059caab3f98a3e04d3afaad19a7d1/src/combineReducers.js#L169

assertReducerShape semble un peu redondant. Mais dans mon cas, les garanties de type breaks... quelle est la chose qu'il essaie d'affirmer ?

Alors, c'est vraiment un problème d'interaction TypeScript ?

Pour être honnête, il semble que ce soit finalement une différence d'approches.

combineReducers() été écrit en JS et essaie de faire des vérifications à l'exécution.

Vous essayez d'effectuer des vérifications statiques dans TS et la déclaration que vous avez écrite ne correspond pas au fonctionnement réel de combineReducers() . Ainsi, dans ce sens, la déclaration de type de votre réducteur de tranche est incorrecte, car elle _peut_ et _sera_ appelée avec undefined lorsqu'elle est utilisée avec combineReducers() .

La ligne spécifique que vous avez appelée vérifie que la valeur de retour de votre réducteur de tranche n'est pas undefined lorsqu'elle est appelée avec une valeur de tranche d'état existante (vraisemblablement significative), alors que assertReducerShape() vérifie qu'elle ne renvoie pas undefined lorsqu'on lui donne une valeur d'état initiale de undefined (c'est-à-dire que le réducteur s'auto-initialise son état) et aussi lorsqu'il est appelé avec un type d'action inconnu (c'est-à-dire que le réducteur renvoie toujours l'état existant par défaut).

si vous n'êtes pas défini, remplacez-le ... dans mon cas, lorsque mon serveur répond sans les données pour une raison quelconque, peut-être parce qu'il n'y a pas de session active, j'ai eu ce problème, donc faire cela dans le réducteur, a fonctionné comme un charme pour moi :

export default function itemReducer(state = initialState.items | undefined, action){ 
    switch(action.type) 
   { 
         case "LOAD_ITEMS_SUCCESS":
               if(action.items==undefined) { 
                        action.items=[]; 
               }
                return action.items

Si le réducteur n'est pas défini, remplacez undefined par un tableau vide et vous n'obtiendrez plus cette erreur.

Acclamations!

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

CellOcean picture CellOcean  ·  3Commentaires

benoneal picture benoneal  ·  3Commentaires

parallelthought picture parallelthought  ·  3Commentaires

rui-ktei picture rui-ktei  ·  3Commentaires

ms88privat picture ms88privat  ·  3Commentaires