Rohan Keskar
Rohan Keskar

Reputation: 79

Unable to access redux state and actions inside nested component in Redux

I have a NoteItem component and it is recursively rendering itself from an array inside itself, But at the topmost component when I use redux functions at the top level parent NoteItem I'm able to use the actions and access the state but inside the nested NoteItem component when I try to use it, It is giving it as undefined, I thought we can use redux wherever we want to use it and access the state and functions as well

This is the parent Sidebar component

Sidebar.js

import React, { Component } from "react";
import { Popover, Tooltip } from "antd";
import { connect } from "react-redux";

import "./sidebar.css";

import settingsIcon from "../icons/settings.svg";

import addIcon from "../icons/plus.svg";

import NoteItem from "../NoteItem/NoteItem";
import { getNotes, createNote } from "../actions/noteActions";
import store from "../store";
import { SET_CONTEXTMENU_VISIBLE, SET_ACTIVE_NOTE } from "../actions/types";

class Sidebar extends Component {
  state = {
    notes: this.props.notes,
    ownUpdate: false,
    xPos: 0,
    yPos: 0,
    mouseDownOnMenu: this.props.mouseDownOnMenu,
    contextMenuVisible: this.props.contextMenuVisible,
  };

  static getDerivedStateFromProps(props, state) {
    if (state.ownUpdate) {
      return {
        ...state,
        ownUpdate: false,
      };
    } else {
      if (props.notes != state.notes) {
        return {
          notes: props.notes,
        };
      }
      if (props.contextMenuVisible != state.contextMenuVisible) {
        return {
          contextMenuVisible: props.contextMenuVisible,
        };
      }
      if (props.mouseDownOnMenu != state.mouseDownOnMenu) {
        return {
          mouseDownOnMenu: props.mouseDownOnMenu,
        };
      }
    }

    return null;
  }

  addNote = () => {
    const newNote = {
      name: "Untitled",
    };

    this.props.createNote(newNote);
  };

  componentDidMount() {
    window.addEventListener("mousedown", (e) => {
      const { mouseDownOnMenu } = this.state;
      if (mouseDownOnMenu) {
        return;
      }
      store.dispatch({
        type: SET_CONTEXTMENU_VISIBLE,
        payload: false,
      });
      store.dispatch({
        type: SET_ACTIVE_NOTE,
        payload: undefined,
      });
    });

    this.props.getNotes();
  }

  componentWillUnmount() {
    window.removeEventListener("mousedown", window);
  }

  addSubNote = (note, path) => {
    this.props.addSubNote(note, path);
  };

  setMouseDownActive = () => {
    this.setState({
      mouseDownOnMenu: !this.state.mouseDownOnMenu,
    });
  };

  render() {
    const { notes, contextMenuVisible, xPos, yPos } = this.state;
    return (
      <div className="sidebar-container">
        <div className="header-container">
          <h4>Username</h4>
          <Tooltip title="Settings" placement="right">
            <img src={settingsIcon} />
          </Tooltip>
        </div>

        <div className="sidebar-content-container">
          <div className="sidebar-controls-container">
            <h4>Notes</h4>
            <div className="add-icon-button" onClick={this.addNote}>
              <img src={addIcon} />
            </div>
          </div>
          <div className="sidebar-page-container">
            {notes &&
              notes.map((note, index) => (
                <NoteItem note={note} key={note._id}></NoteItem> // I can use all the functions and 
 // access the state in these components
              ))}
          </div>
        </div>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    notes: state.note.notes,
    contextMenuVisible: state.note.contextMenuVisible,
    mouseDownOnMenu: state.note.mouseDownOnMenu,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    createNote: (noteData) => dispatch(createNote(noteData)),
    getNotes: () => dispatch(getNotes()),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Sidebar);

This is my NoteItem component

NoteItem.js

import React, { Component } from "react";
import "./noteitem.css";
import pageIcon from "../icons/file-text.svg";

import { connect } from "react-redux";

import addIcon from "../icons/plus.svg";
import caretIcon from "../icons/play.svg";

import { Popover, Button, Input, Menu } from "antd";
import { addSubNote, deleteNote } from "../actions/noteActions";
import { DeleteOutlined, EditOutlined, LinkOutlined } from "@ant-design/icons";
import {
  SET_ACTIVE_NOTE,
  SET_CONTEXTMENU_VISIBLE,
  SET_MOUSEDOWNON_MENU,
} from "../actions/types";
import axios from "axios";
import apiUrl from "../utils/getApiUrl";
import store from "../store";

class NoteItem extends Component {
  state = {
    subNoteOpen: false,
    contextMenuVisible: this.props.contextMenuVisible,
    xPos: 0,
    yPos: 0,
    deleteNoteActive: false,
    renamePopupVisible: false,
    note: this.props.note,
    ownUpdate: false,
    activeNote: this.props.activeNote,
    subNotes: [],
    mouseDownOnMenu: this.props.mouseDownOnMenu,
  };

  componentDidUpdate(prevProps, prevState, snap) {
    if (
      prevProps.activeNote != this.props.activeNote &&
      this.props.activeNote
    ) {
      this.setState({
        activeNote: this.props.activeNote,
        ownUpdate: true,
      });
    }
  }

  static getDerivedStateFromProps(props, state) {
    if (state.ownUpdate) {
      return {
        ...state,
        ownUpdate: false,
      };
    } else {
      if (props.note != state.note) {
        return {
          note: props.note,
        };
      }
      if (props.contextMenuVisible != state.contextMenuVisible) {
        return {
          contextMenuVisible: props.contextMenuVisible,
        };
      }
      if (props.mouseDownOnMenu != state.mouseDownOnMenu) {
        return {
          mouseDownOnMenu: props.mouseDownOnMenu,
        };
      }
      if (props.activeNote != state.activeNote) {
        return {
          activeNote: props.activeNote,
        };
      }
    }

    return null;
  }

  getSubNotes = async () => {
    try {
      const { note } = this.state;
      if (note) {
        const subNotes = await axios.get(`${apiUrl}/note/sub-notes`, {
          headers: {
            "Content-Type": "application/json",
          },
          params: {
            id: note._id,
          },
        });
        this.setState({
          subNotes: subNotes.data,
        });
      }
    } catch (err) {
      console.log(err);
    }
  };

  addNewSubNote = async (note) => {
    try {
      const subNote = await axios.post(
        `${apiUrl}/note/subnote/new`,
        {
          note: {
            name: "Untitled",
          },
          path: note.path,
        },
        {
          headers: {
            "Content-Type": "application/json",
          },
        }
      );
      this.setState({
        subNotes: [...this.state.subNotes, subNote.data],
        subNoteOpen: true,
      });
    } catch (err) {
      console.log(err);
    }
  };

  getRenameNoteInput = () => {
    return (
      <div className="rename-note-container">
        <Input placeholder="Enter name" />
        <Button type="primary">Save</Button>
      </div>
    );
  };

  toggleSubNote = () => {
    this.setState(
      {
        subNoteOpen: !this.state.subNoteOpen,
      },
      () => {
        if (this.state.subNoteOpen) {
          this.getSubNotes();
        }
      }
    );
  };

  toggleContextMenu = (e, note) => {
    e.preventDefault();
    var body = document.body,
      html = document.documentElement;
    var height = Math.max(
      body.scrollHeight,
      body.offsetHeight,
      html.clientHeight,
      html.scrollHeight,
      html.offsetHeight
    );
    if (e.pageY > height - 100) {
      this.setState(
        {
          xPos: e.pageX,
          yPos: e.pageY - 130,
        },
        () => {
          store.dispatch({
            type: SET_ACTIVE_NOTE,
            payload: note,
          });
        }
      );
    } else {
      this.setState(
        {
          xPos: e.pageX,
          yPos: e.pageY,
        },
        () => {
          store.dispatch({
            type: SET_ACTIVE_NOTE,
            payload: note,
          });
        }
      );
    }
    store.dispatch({
      type: SET_CONTEXTMENU_VISIBLE,
      payload: true,
    });
  };

  deleteNote = ({ domEvent }) => {
    domEvent.preventDefault();
    store.dispatch({
      type: SET_MOUSEDOWNON_MENU,
      payload: true,
    });
    store.dispatch({
      type: SET_CONTEXTMENU_VISIBLE,
      payload: false,
    });
    const { note } = this.state;
    console.log(this.props);
    this.props.deleteNote(note);
  };

  setMouseDownActive = () => {
    store.dispatch({
      type: SET_MOUSEDOWNON_MENU,
      payload: !this.state.mouseDownOnMenu,
    });
  };

  render() {
    const {
      subNoteOpen,
      note,
      renamePopupVisible,
      activeNote,
      subNotes,
      contextMenuVisible,
      xPos,
      yPos,
    } = this.state;
    return (
      <div style={{ width: "100%" }}>
        {activeNote && contextMenuVisible && activeNote._id == note._id && (
          <div
            className="context-menu"
            style={{ position: "fixed", top: yPos, left: xPos }}
          >
            <Menu
              style={{ width: "200px", borderRadius: "3px" }}
              onMouseDown={this.setMouseDownActive}
              onMouseUp={this.setMouseDownActive}
            >
              <Menu.Item icon={<EditOutlined />}>Rename</Menu.Item>
              <Menu.Item icon={<LinkOutlined />}>Copy Link</Menu.Item>
              <Menu.Item icon={<DeleteOutlined />} onClick={this.deleteNote}>
                Delete
              </Menu.Item>
            </Menu>
          </div>
        )}
        <Popover
          content={this.getRenameNoteInput()}
          placement="bottom"
          overlayClassName="no-arrow"
          visible={renamePopupVisible}
        >
          <div
            className={
              activeNote && activeNote._id == note._id
                ? "page-item page-item-active"
                : "page-item"
            }
            onContextMenu={(e) => this.toggleContextMenu(e, note)}
          >
            <img
              src={caretIcon}
              onClick={this.toggleSubNote}
              className={subNoteOpen ? "caret-open" : "caret"}
            />
            <img src={pageIcon} />

            <span>{note.name}</span>
            <div
              className="add-page-button"
              onClick={() => this.addNewSubNote(note)}
            >
              <img src={addIcon} />
            </div>
          </div>
        </Popover>
        <div
          className={
            subNoteOpen
              ? "sub-page-container subpage-open"
              : "sub-page-container"
          }
        >
          {subNotes.length > 0 ? (
            <div>
              {subNotes.map((subNote) => (
                <NoteItem note={subNote} key={subNote._id} /> // I can't use any redux functions or 
 // access redux state in these components
              ))}
            </div>
          ) : (
            <p className="empty-text">Empty</p>
          )}
        </div>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    activeNote: state.note.activeNote,
    contextMenuVisible: state.note.contextMenuVisible,
    mouseDownOnMenu: state.note.mouseDownOnMenu,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    addSubNote: (note, path) => dispatch(addSubNote(note, path)),
    deleteNote: (note) => dispatch(deleteNote(note)),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(NoteItem);


Upvotes: 1

Views: 215

Answers (1)

phry
phry

Reputation: 44086

Your connected default exported NoteItem component receives the props - but then it is recursively rendering the unconnected version of NoteItem. That version has no knowledge of the redux store.

You could do something like

const ConnectedNoteItem = connect(mapStateToProps, mapDispatchToProps)(
class NoteItem extends Component {
// ... use ConnectedNoteItem in here
})
export default ConnectedNoteItem

Upvotes: 1

Related Questions