0

I'm using memoize-one on a React component that is basically a table with a rows that can be filtered.

Memoize works great for the filtering but when I want to insert a new row, it won't show up on the table until I either reload the page or use the filter.

If I check the state, the new row's data is in it, so presumably what is happening is that memoize is not allowing the component to re-render even if the state has changed.

Something interesting is that the Delete function works, I am able to delete a row by removing its data from the state and it will re-render to reflect the changes...

Here's the part of the code I consider relevant but if you would like to see more, let me know:

import React, { Component } from "react";
import memoize from "memoize-one";
import moment from "moment";
import {
  Alert,
  Card,
  Accordion,
  Button,
  Table,
  Spinner,
} from "react-bootstrap";
import PropTypes from "prop-types";
import { getRoleMembersDetailed } from "../libs/permissions-manager-client-v1.0";

 import RoleMember from "./RoleMember";
import CreateMemberModal from "./CreateMemberModal";

class RoleContainer extends Component {
  filter = memoize((roleMembers, searchValue, searchCriterion) => {
    const searchBy = searchCriterion || "alias";
    return roleMembers.filter((item) => {
      if (item[searchBy]) {
        if (searchValue === "") {
          return true;
        }

        const value = searchValue.toLowerCase();

        if (searchBy !== "timestamp") {
          const target = item[searchBy].toLowerCase();

          return target.includes(value);
        }
        // Case for timestamp
        const target = moment(Number(item[searchBy]))
          .format("MMM DD, YYYY")
          .toLowerCase();

        return target.includes(value);
      }
      return false;
    });
  });

  constructor(props) {
    super(props);
    this.state = {
      collapsed: true,
      roleAttributes: [],
      roleMembers: [],
      isLoading: true,
    };
  }

  componentDidMount = async () => {
    const roleMembers = Object.values(await this.fetchRoleMembers());
    roleMembers.forEach((e) => {
      e.alias = e.alias.toLowerCase();
      return null;
    });

    roleMembers.sort((a, b) => {
      if (a.alias < b.alias) {
        return -1;
      }
      if (a.alias > b.alias) {
        return 1;
      }
      return 0;
    });

    // TODO - This logic should be replaced with an API call that describes the roleAttributes.
    let roleAttributes = Object.values(roleMembers);
    roleAttributes = Object.keys(roleAttributes[0]);
    this.setState({
      roleMembers,
      roleAttributes,
      isLoading: false,
    });
  };

  fetchRoleMembers = async () => {
    const { roleAttributeName } = this.props;
    return getRoleMembersDetailed(roleAttributeName);
  };

  createRoleMember = (newRoleMembers) => {
    const { roleMembers } = this.state;

    newRoleMembers.forEach((e) => {
      roleMembers.push(e);
    });

    this.setState(
      () => {
        roleMembers.sort((a, b) => {
          if (a.alias < b.alias) {
            return -1;
          }
          if (a.alias > b.alias) {
            return 1;
          }
          return 0;
        });
        return { roleMembers };
      },
      () => {
        console.log("sss", this.state);
      }
    );
  };

  deleteRoleMember = (alias) => {
    this.setState((prevState) => {
      const { roleMembers } = prevState;
      return {
        roleMembers: roleMembers.filter((member) => member.alias !== alias),
      };
    });
  };
render() {
const {
  role,
  roleAttributeName,
  searchValue,
  searchCriterion,
  userCanEdit,
} = this.props;
const { collapsed, isLoading, roleAttributes, roleMembers } = 
this.state;
const filteredRoleMembers = this.filter(
  roleMembers,
  searchValue,
  searchCriterion
);
return (
// continues...

I don't know if it's obvious but there are two functions called filter: this.filter that belongs to memoize and Array.prototype.filter().

I did look around and found these post that says Memoize can be overridden:

If you’ve ran into a UI bug, it is simple to just return false from myComparison to temporarily override the memoization, forcing a refresh on every re-render and returning to the default component behaviour.

But I'm not sure what they mean with "return false from component"

4
  • 1
    This code looks like it could be made much simpler with React hooks (incl. useMemo!), if that's an option... Commented Sep 8, 2020 at 17:24
  • I don't see where you use this.filter? Commented Sep 8, 2020 at 17:28
  • Thanks for the suggestion, I'm reading about useMemo and looks better indeed. Would I run into this same UI bug with it? Any idea? Commented Sep 8, 2020 at 17:28
  • @hyperdrive, sorry just added the part where it's used... Commented Sep 8, 2020 at 17:34

1 Answer 1

1

Here's a refactoring of your code to idiomatic React Hooks style (naturally dry-coded).

Note how filtering and sorting the role members is done using useMemo() in a way that doesn't modify state; that's because they can be always recomputed from the stateful data. So long as the useMemo()s' deps array is kept in sync (there're ESLint rules to help with this), this should work with no extra re-renders. :)

Similarly, if you use useCallback (which is a special case of useMemo), you need to keep their deps arrays in sync. If you don't use useCallback, those callbacks may cause re-renders since their identity changes per-render.

import React, { Component } from "react";
import moment from "moment";
import { getRoleMembersDetailed } from "../libs/permissions-manager-client-v1.0";

function filterRoleMembers(
  roleMembers,
  searchValue,
  searchCriterion,
) {
  const searchBy = searchCriterion || "alias";
  return roleMembers.filter((item) => {
    if (item[searchBy]) {
      if (searchValue === "") {
        return true;
      }

      const value = searchValue.toLowerCase();

      if (searchBy !== "timestamp") {
        const target = item[searchBy].toLowerCase();

        return target.includes(value);
      }
      // Case for timestamp
      const target = moment(Number(item[searchBy]))
        .format("MMM DD, YYYY")
        .toLowerCase();

      return target.includes(value);
    }
    return false;
  });
}

// TODO: maybe use lodash's `sortBy`?
function compareByAlias(a, b) {
  if (a.alias < b.alias) {
    return -1;
  }
  if (a.alias > b.alias) {
    return 1;
  }
  return 0;
}

async function fetchRoleMembers(roleAttributeName) {
  return getRoleMembersDetailed(roleAttributeName);
}

async function loadData(roleAttributeName) {
  const roleMembers = Object.values(
    await fetchRoleMembers(roleAttributeName),
  );
  roleMembers.forEach((e) => {
    e.alias = e.alias.toLowerCase();
  });

  // TODO - This logic should be replaced with an API call that describes the roleAttributes.
  let roleAttributes = Object.values(roleMembers);
  roleAttributes = Object.keys(roleAttributes[0]);
  return {
    roleMembers,
    roleAttributes,
  };
}

const RoleContainer = ({
  role,
  roleAttributeName,
  searchValue,
  searchCriterion,
  userCanEdit,
}) => {
  const [collapsed, setCollapsed] = React.useState(true);
  const [isLoading, setIsLoading] = React.useState(true);
  const [roleAttributes, setRoleAttributes] = React.useState([]);
  const [roleMembers, setRoleMembers] = React.useState([]);
  React.useEffect(() => {
    loadData(roleAttributeName).then(
      ({ roleMembers, roleAttributes }) => {
        setRoleAttributes(roleAttributes);
        setRoleMembers(roleMembers);
        setIsLoading(false);
      },
    );
  }, [roleAttributeName]);
  const createRoleMember = React.useCallback(
    (newRoleMembers) => {
      const updatedRoleMembers = roleMembers.concat(newRoleMembers);
      setRoleMembers(updatedRoleMembers);
    },
    [roleMembers],
  );
  const deleteRoleMember = React.useCallback(
    (alias) => {
      const updatedRoleMembers = roleMembers.filter(
        (member) => member.alias !== alias,
      );
      setRoleMembers(updatedRoleMembers);
    },
    [roleMembers],
  );
  const filteredRoleMembers = React.useMemo(
    () =>
      filterRoleMembers(roleMembers, searchValue, searchCriterion),
    [roleMembers, searchValue, searchCriterion],
  );
  const sortedRoleMembers = React.useMemo(
    () => [].concat(filteredRoleMembers).sort(compareByAlias),
    [filteredRoleMembers],
  );
  return <>{JSON.stringify(sortedRoleMembers)}</>;
};
Sign up to request clarification or add additional context in comments.

1 Comment

Hey thank you so much for taking the time of explaining this concept ad rewriting my code. I was just able to fix my initial issue but it feels better to use this solution as it's built into React Hooks... I'm pretty new to React and haven't gotten into use Hooks but I keep reading about it and seems like it's time to jump in! haha thanks again AKX!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.