moonwalker
moonwalker

Reputation: 1177

React context state property is undefined

I'm trying to build a web application menu dynamically using react's new context API, json as data, Providers, Consumers etc. I can't figure out an error I get. But first here's some code:

Context file includes:

import React from 'react';

export const MenuContext = React.createContext();

class MenuProvider extends React.Component {
  state = {
    menu: {}
  };
  actions = {
    fetchMenu: async() => {
      const response = await fetch('/admin/json/menu.json');
      const body = await response.json();
      this.setState({
        menu: body
      });
    }
  }
  componentDidMount() {
    this.actions.fetchMenu();
  }
  render() {
    return(
      <MenuContext.Provider value={{state: this.state, actions: this.actions}}>
        {this.props.children}
      </MenuContext.Provider>
    )
  }
}

export default MenuProvider;

The menu component consumes this context as follows:

import React from 'react';
import {v1 as uuid} from 'uuid';

import MenuProvider, {MenuContext} from './contexts/menu';

import MenuDropdown from './parts/menu-dropdown';
import MenuItem from './parts/menu-item';

export default class SideMenu extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return(
      <header>
        <MenuProvider>
          <MenuContext.Consumer>
            {({state}) => (
              <React.Fragment>
                <div className="cnt-menu-head">
                  <h6>{state.menu.heading}</h6>
                </div>
                <div className="cnt-menu">
                  <ul className="nav nav-pills flex-column">
                    {state.menu.sections.map((section, i) => {
                      if (section.type === 'link') {
                        return <MenuItem exact={section.exact} linkTo={section.linkTo} linkText={section.linkText}/>
                      } else if (section.type === 'dropdown') {
                        var keyedLinks = section.links.map((link) => {
                          link.key = uuid();
                          return link;
                        });
                        return <MenuDropdown key={uuid} linkText={section.linkText} links={keyedLinks}/>
                      }
                    })}
                  </ul>
                </div>
              </React.Fragment>
            )}
          </MenuContext.Consumer>
        </MenuProvider>
      </header>
    );
  };
};

menu.json file has this data:

{
  "heading": "Menu Heading",
  "sections": [
    {
      "type": "link",
      "linkText": "A Link",
      "linkTo": "/",
      "exact": true
    },
    {
      "type": "dropdown",
      "linkText": "System",
      "links": [
        {
          "linkText": "System Table",
          "linkTo": "/table/system",
          "exact": false
        },
        {
          "linkText": "Add System",
          "linkTo": "/form/addsystem",
          "exact": false
        }
      ]
    }
  ]
}

Versions of packages I use in package.json are:

"devDependencies": {
    "babel-plugin-transform-class-properties": "^6.24.1",
    "babel-preset-es2016": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "react": "^16.4.1",
    "react-dom": "^16.4.1",
    "react-router-dom": "^4.3.1",
    "uuid": "^3.3.0"
},

And .babelrc file is:

{
  "presets": [
    ["es2016"],
    "react"
  ],
  "plugins": [
    "transform-class-properties"
  ]
}

Compilation of this application runs without errors, though I get a javascript error in console when I run it:

Uncaught TypeError: Cannot read property 'map' of undefined

The property in question here is state.menu.sections which I want to loop through and render based on type. If I comment out the part I loop state.menu.sections with map, the part <h6>{state.menu.heading}</h6> works flawlessly and renders <h6>Menu Heading</h6> without any errors. What may be the problem when I loop the sections array?

Upvotes: 1

Views: 6579

Answers (1)

Lpaydat
Lpaydat

Reputation: 436

Try this code:

import React from 'react';

export const MenuContext = React.createContext();

class MenuProvider extends React.Component {
  state = {
    menu: { heading: '', sections: [] }
  };
  actions = {
    fetchMenu: async() => {
      const response = await fetch('/admin/json/menu.json');
      const body = await response.json();
      this.setState({
        menu: body
      });
    }
  }
  componentDidMount() {
    this.actions.fetchMenu();
  }
  render() {
    return(
      <MenuContext.Provider value={{state: this.state, actions: this.actions}}>
        {this.props.children}
      </MenuContext.Provider>
    )
  }
}

export default MenuProvider;

The difference is menu in state, instead of empty menu object, you need to provide it with default values.

NOTE: I not sure about the error, but I think it is because javascript try to use map on state.menu.sections before the componentDidMount set new state to your state (so the state.menu.sections will be undefined by default)

Upvotes: 5

Related Questions