0

I've built a toolbar that allows a user to filter data via dates and a search bar. The data that's filtered (expenses) is being fed into a child component and although those props are being changed on button click, the child component itself is not being re-rendered and thus is displaying the original unfiltered data.

This is the parent component:

const Row1 = () => {
  const isAboveMediumScreen = useMediaQuery("(min-width: 1200px)");
  let { data: expenses, isLoading } = useGetExpensesQuery();
  const { data: categories } = useGetCategoriesQuery();
  const [start, setStart] = React.useState<Dayjs | null>(null);
  const [end, setEnd] = React.useState<Dayjs | null>(null);
  const [category, setCategory] = React.useState<string>("");

  useEffect(() => {
    setStart(getEarliest(expenses));
    setEnd(getLatest(expenses));
    setCategory("All categories");
  }, [expenses]);

  const handleChange = (event: SelectChangeEvent<string>) => {
    setCategory(event.target.value);
  };

  const handleFilter = () => {
    console.log(start, end, category);
    expenses = expenses?.filter((expense) => {
      let date = dayjs(expense.date);
      if (!date.isBefore(start) && !date.isAfter(end)) {
        return date;
      }
    });

    console.log(expenses);
  };

  if (!expenses) {
    return null;
  }


  return (
            <LineChartComponent expenses={expenses} isLoading={isLoading} />

  );
};

export default Row1;

And the child component (LineChartComponent) is as follows:

const LineChartComponent: React.FC<{
  expenses: Expense[];
  isLoading: boolean;
}> = ({ expenses, isLoading }) => {
  const finalDate = useMemo(() => {
    console.log("line chart expenses", expenses);
    const finalDate: { date: string; expenses: number }[] = [];

    if (expenses) {
      const orderedDates = expenses
        .map((expense) => ({
          ...expense,
          date: new Date(expense.date!).getMonth(),
        }))
        .sort((a, b) => a.date - b.date);

      const reduced = orderedDates.reduce(
        (map, { date, price, subCategory, userId, category }) =>
          map.set(date, (map.get(date) ?? 0) + price),
        new Map()
      );

      reduced.forEach((price, date) => {
        finalDate.push({
          expenses: price,
          date: monthNames[date].substring(0, 3),
        });
      });
      return finalDate;
    }
    return [];
  }, [expenses]);

  const renderLegendText = (value: string) => {
    return <span>{value}($)</span>;
  };

  return (
    <ResponsiveContainer width="100%" height="100%">
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          width: "100%",
          height: "100%",
        }}
      >
        {isLoading ? (
          <Typography variant="h4" sx={{ color: "#12efc8" }}>
            Loading...
          </Typography>
        ) : (
          <LineChart
            width={400}
            height={230}
            data={finalDate}
            margin={{
              // top: 5,
              right: 30,
              // left: 20,
              // bottom: 5,
            }}
          >
            <CartesianGrid strokeDasharray="3 3" />
            <XAxis dataKey="date" />
            <YAxis type="number" domain={["dataMin-100", "dataMax+100"]} />
            <Tooltip />
            <Legend formatter={renderLegendText} />
            <Line type="monotone" dataKey="expenses" stroke="#12efc8" />
          </LineChart>
        )}
      </div>
    </ResponsiveContainer>
  );
};

export default LineChartComponent;

I'm using useMemo to in my LineChartComponent because I want to prevent the same logic on re-render and I'm not sure how to incorporate useEffect with useMemo. I'm thinking useEffect here to test for prop change but I'm not sure if that's correct

1 Answer 1

1

Line chart component will not rerender because as far it react is concerned expenses is not changing. Guessing expenses is stateful data since it's from a hook. In react you cannot assign to a stateful variable directly. You cannot mutate it, that's why state provides the value and a setter.

In this case you could change handleFilter to getFilteredData where you return a new variable that represents the expenses data with the filter operations applied to it.

const getFilteredData = () =>
  expenses?.filter((expense) => {
    let date = dayjs(expense.date);
    if (!date.isBefore(start) && !date.isAfter(end)) {
      return date;
    }
  });

And then you call it to pass it to the chart component

<LineChartComponent expenses={getFilteredData()} isLoading={isLoading} />;

At this point you don't really need to make it a function you could just the value declared as filteredData=expenses?.map(....) and pass that directly.

But you could also, if the performance need is there, wrap it in useMemo to only run it when whatever you put in deps changes

const filteredData = useMemo(
  () =>
    expenses?.filter((expense) => {
      let date = dayjs(expense.date);
      if (!date.isBefore(start) && !date.isAfter(end)) {
        return date;
      }
    }),
  [expenses, start, end],
);

Hope that helps as a starting point.

Sign up to request clarification or add additional context in comments.

1 Comment

Appreciate the answer! Since I posted, I have added an expense state and have setExpenses to a new array onClick, it's solved the issue but I've run into others haha

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.