TomSawyer
TomSawyer

Reputation: 3820

React - Proper way to render dynamic content?

I want to make a modal view has dynamic content by injecting a component to it.

class RootView extends Component {
    state = {
        modalBody: null
    }

    setModalBody = (body) => {
        this.setState({modalBody: body})
    }

    render() {
        return(<ContextProvider value={this.setModalBody}><Modal>{this.state.modalBody}</Modal></ContextProvider>)
    }
}

Then inside any children view i use setState to change parent modalBody

The modalBody can be setted on each route, which means the modalBody can be input list, selection list or text only. So the modalBody must have its state for controlling these inputs.

By this way, it renders ok, but the dynamic content couldn't be updated after state changed. The parent's dynamic content couldn't receive the ChildView new state, i have to setModalBody again and again after it rerendered.

For example, if input in modalBody has changed, the parent couldn't be updated.

class ChildView extends Component {
    state = {
        inputValue: null
    }

    handleChange = (e) => {
        this.setState({inputValue: e.target.value})
    }


    setModalBody(body) {
        this.props.context.setModalBody(<input value={this.state.inputValue} onChange={this.handleChange} />)
    }

    render() {
        return(<Modal>{this.state.modalBody}</Modal>)
    }
}

Full code: https://codesandbox.io/s/lp5p20mx1m Any proper way to render dynamic content to parent?

Upvotes: 1

Views: 8670

Answers (1)

Matt Carlotta
Matt Carlotta

Reputation: 19772

I'm not sure why you'd need to create a parent Modal component, when you can make the Modal a simple reusable child component.

See here for a detailed explanation on how to achieve a stateful parent that controls a child modal.


However, if you MUST have a parent Modal component, then you can create a render prop to pass down props to be used by its children.

Working example:

Edit Parent Modal

components/Modal.js (parent component -- this has a lot of smaller components that were separated for reusability and ease of understanding -- they're basically simple divs with some styles attached -- see notes below)

import React, { Fragment, Component } from "react";
import PropTypes from "prop-types";
import BackgroundOverlay from "../BackgroundOverlay"; // grey background
import ClickHandler from "../ClickHandler"; // handles clicks outside of the modal
import Container from "../Container"; // contains the modal and background
import Content from "../Content"; // renders the "children" placed inside of <Modal>...</Modal>
import ModalContainer from "../ModalContainer"; // places the modal in the center of the page


class Modal extends Component {
  state = { isOpen: false };

  handleOpenModal = () => {
    this.setState({ isOpen: true });
  };

  handleCloseModal = () => {
    this.setState({ isOpen: false });
  };


  // this is a ternary operator (shorthand for "if/else" -- if cond ? then : else)
  // below can be read like: if isOpen is true, then render the modal,
  // else render whatever the child component is returning (in this case,
  // initially returning an "Open Modal" button)
  render = () =>
    this.state.isOpen ? (
      <Container>
        <BackgroundOverlay />
        <ModalContainer>
          <ClickHandler
            isOpen={this.state.isOpen}
            closeModal={this.handleCloseModal}
          >
            <Content>
              {this.props.children({
                isOpen: this.state.isOpen,
                onCloseModal: this.handleCloseModal,
                onOpenModal: this.handleOpenModal
              })}
            </Content>
          </ClickHandler>
        </ModalContainer>
      </Container>
    ) : (
      <Fragment>
        {this.props.children({
          isOpen: this.state.isOpen,
          onCloseModal: this.handleCloseModal,
          onOpenModal: this.handleOpenModal
        })}
      </Fragment>
    );
}

// these proptype declarations are to ensure that passed down props are 
// consistent and are defined as expected
Modal.propTypes = {
  children: PropTypes.func.isRequired // children must be a function
};

export default Modal;

components/Example.js (child component accepting isOpen, onCloseModal and onOpenModal from the parent -- with this approach, as you'll notice, there's duplicate isOpen logic. While this approach gives you full control over the parent, it's repetitive. However, you can simplify your components by moving the "Open Modal" button logic to the parent, and passing in a prop like <Modal btnTitle="Open Modal"> to make it somewhat flexible, BUT you'll still lose some control of what's being initially rendered when isOpen is false.)

import React, { Fragment } from "react";
import Modal from "../Modal";
import "./styles.css";

const Example = () => (
  <div className="example">
    <h2>Parent Modal Example</h2>
    <Modal>
      {({ isOpen, onCloseModal, onOpenModal }) =>
        isOpen ? (
          <Fragment>
            <h1 className="title">Hello!</h1>
            <p className="subtitle">There are two ways to close this modal</p>
            <ul>
              <li>Click outside of this modal in the grey overlay area.</li>
              <li>Click the close button below.</li>
            </ul>
            <button
              className="uk-button uk-button-danger uk-button-small"
              onClick={onCloseModal}
            >
              Close
            </button>
          </Fragment>
        ) : (
          <button
            className="uk-button uk-button-primary uk-button-small"
            onClick={onOpenModal}
          >
            Open Modal
          </button>
        )
      }
    </Modal>
  </div>
);

export default Example;

Upvotes: 1

Related Questions