Search code examples
javascriptreactjsecmascript-6setstatereact-table

Prevent child's state from reset after parent component state changes also get the values of all child components:ReactJS+ Typescript


I am a bit new to react and I am stuck in this situation where I am implementing custom dropdown filter for a table in react. I have set of dropdown values for each column and there is a Apply button.

I have maintained a child component for this which takes in drop down values and sends the selected one's back to parent. Then I call a back-end API that gives me filtered data which in-turn sets parents state . The problem here is the checkbox values inside dropdown is lost after I get the data and set the parent state.

Each child components has as a set of checkboxes , an Apply and a clear button. So on click of Apply , I have to send the checked one's to the parent or in general whichever the checked one's without losing the previous content.

I am unable to understand why am I losing the checkbox values?

It would be of great help if someone can help me out with this

Sand box: https://codesandbox.io/s/nervous-elgamal-0zztb

I have added the sandbox link with proper comments. Please have a look. I am a bit new to react.

Help would be really appreciated

Parent

import * as React from "react";
import { render } from "react-dom";
import ReactTable from "react-table";
import "./styles.css";
import "react-table/react-table.css";
import Child from "./Child";
interface IState {
  data: {}[];
  columns: {}[];
  selectedValues: {};
  optionsForColumns: {};
}

interface IProps {}

export default class App extends React.Component<IProps, IState> {

  // Here I have  hardcoded the values, but data and optionsForColumns comes from the backend and it is set inside componentDidMount
  constructor(props: any) {
    super(props);
    this.state = {
      data: [
        { firstName: "Jack", status: "Submitted", age: "14" },
        { firstName: "Simon", status: "Pending", age: "15" }
      ],
      selectedValues: {},
      columns: [],
      optionsForColumns: {
        firstName: [{ Jack: "4" }, { Simon: "5" }],
        status: [{ Submitted: "5" }, { Pending: "7" }]
      }
    };
  }

  // Get the values for checkboxes that will be sent to child
  getValuesFromKey = (key: any) => {
    let data: any = this.state.optionsForColumns[key];
    let result = data.map((value: any) => {
      let keys = Object.keys(value);
      return {
        field: keys[0],
        checked: false
      };
    });
    return result;
  };

  // Get the consolidated values from child and then pass it for server side filtering
  handleFilter = (fieldName: any, selectedValue: any, modifiedObj: any) => 
  {
    this.setState(
      {
        selectedValues: {
          ...this.state.selectedValues,
          [fieldName]: selectedValue
        }
      },
      () => this.handleColumnFilter(this.state.selectedValues)
    );
  };

  // Function that will make server call based on the checked values from child
  handleColumnFilter = (values: any) => {
    // server side code for filtering
    // After this checkbox content is lost
  };

  // Function where I configure the columns array for the table . (Also data and column fiter values will be set here, in this case I have hardcoded inside constructor)
  componentDidMount() {
    let columns = [
      {
        Header: () => (
          <div>
            <div>
              <Child
                key="firstName"
                name="firstName"
                options={this.getValuesFromKey("firstName")}
                handleFilter={this.handleFilter}
              />
            </div>
            <span>First Name</span>
          </div>
        ),
        accessor: "firstName"
      },
      {
        Header: () => (
          <div>
            <div>
              <Child
                key="status"
                name="status"
                options={this.getValuesFromKey("status")}
                handleFilter={this.handleFilter}
              />
            </div>
            <span>Status</span>
          </div>
        ),
        accessor: "status",
      },
      {
        Header: "Age",
        accessor: "age"
      }
    ];
    this.setState({ columns });
  }

  //Rendering the data table
  render() {
    const { data, columns } = this.state;
    return (
      <div>
        <ReactTable
          data={data}
          columns={columns}
        />
      </div>
    );
  }
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);

Child


import * as React from "react";
import { Button, Checkbox, Icon } from "semantic-ui-react";
interface IProps {
  options: any;
  name: string;
  handleFilter(val1: any, val2: any, val3: void): void;
}
interface IState {
  showList: boolean;
  selected: [];
  checkboxOptions: any;
}
export default class Child extends React.Component<IProps, IState> {
  constructor(props: any) {
    super(props);
    this.state = {
      selected: [],
      showList: false,
      checkboxOptions: this.props.options.map((option: any) => option.checked)
    };
  }

  // Checkbox change handler
  handleValueChange = (event: React.FormEvent<HTMLInputElement>, data: any) => {
    const i = this.props.options.findIndex(
      (item: any) => item.field === data.name
    );
    const optionsArr = this.state.checkboxOptions.map(
      (prevState: any, si: any) => (si === i ? !prevState : prevState)
    );
    this.setState({ checkboxOptions: optionsArr });
  };

  //Passing the checked values back to parent
  passSelectionToParent = (event: any) => {
    event.preventDefault();
    const result = this.props.options.map((item: any, i: any) =>
      Object.assign({}, item, {
        checked: this.state.checkboxOptions[i]
      })
    );
    const selected = result
      .filter((res: any) => res.checked)
      .map((ele: any) => ele.field);
    console.log(selected);
    this.props.handleFilter(this.props.name, selected, result);
  };

  //Show/Hide filter
  toggleList = () => {
    this.setState(prevState => ({ showList: !prevState.showList }));
  };

  //Rendering the checkboxes based on the local state, but still it gets lost after filtering happens
  render() {
    let { showList } = this.state;
    let visibleFlag: string;
    if (showList === true) visibleFlag = "visible";
    else visibleFlag = "";
    return (
      <div>
        <div style={{ position: "absolute" }}>
          <div
            className={"ui scrolling dropdown column-settings " + visibleFlag}
          >
            <Icon className="filter" onClick={this.toggleList} />
            <div className={"menu transition " + visibleFlag}>
              <div className="menu-item-holder">
                {this.props.options.map((item: any, i: number) => (
                  <div className="menu-item" key={i}>
                    <Checkbox
                      name={item.field}
                      onChange={this.handleValueChange}
                      label={item.field}
                      checked={this.state.checkboxOptions[i]}
                    />
                  </div>
                ))}
              </div>
              <div className="menu-btn-holder">
                <Button size="small" onClick={this.passSelectionToParent}>
                  Apply
                </Button>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }
}





Solution

  • This appears to be a case of state being managed in an inconvenient way. Currently, the state is managed at the Child level, but it would be easier to manage at the Parent level. This is known as lifting state up in React.

    The gist - the shared state is managed in the Parent component, and it's updated by calling a function passed to the Child component. When Apply is clicked, the selected radio value is passed up to the Parent, which merges the new selection into the shared state.

    I have created a minimal example of your code, showing how we can lift state up from the Child to the Parent component. I'm also using a few new-ish features of React, like useState to simplify the Child component.

    // Child Component
    const Child = ({name, options, updateSelections}) => {
      const [selected, setSelected] = React.useState([]);
      const handleChange = (event) => {
        let updated;
        if (event.target.checked) {
          updated = [...selected, event.target.value];
        } else {
          updated = selected.filter(v => v !== event.target.value);
        }
        setSelected(updated);
      }
      
      const passSelectionToParent = (event) => {
        event.preventDefault();
        updateSelections(name, selected);
      }
    
      return (
        <form>
          {options.map(item => (
            <label for={name}>
              <input
                key={name}
                type="checkbox"
                name={item}
                value={item}
                onChange={handleChange}
              />
              {item}
            </label>
          ))}
          <button onClick={passSelectionToParent}>Apply</button>
        </form>
      )
    }
    
    // Parent Component
    class Parent extends React.Component {
      constructor(props) {
        super(props);
        this.fields = ["firstName", "status"],
        this.state = {
          selected: {}
        };
      }
      
      getValuesFromKey = (data, key) => {
        return data.map(item => item[key]);
      }
      
      updateSelections = (name, selection) => {
        this.setState({
          selected: {...this.state.selected, [name]: selection}
        }, () => console.log(this.state.selected));
      }
    
      render() {
        return (
          <div>
            {this.fields.map(field => (
              <Child
                key={field}
                name={field}
                options={this.getValuesFromKey(this.props.data, field)} 
                updateSelections={this.updateSelections}
              />
            ))}
          </div>
        )
      }
    }
    
    const data = [
      { firstName: "Jack", status: "Submitted" },
      { firstName: "Simon", status: "Pending" },
      { firstName: "Pete", status: "Approved" },
      { firstName: "Lucas", status: "Rejected" }
    ];
    
    ReactDOM.render(<Parent data={data}/>, document.getElementById("root"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
    
    <div id="root"></div>