1

I've been trying to make a chart with data fetched from an API that returns data as follows:

{
  "totalAmount": 230,
  "reportDate": "2020-03-05"
},
{
  "totalAmount": 310,
  "reportDate": "2020-03-06"
}
...

The date string is too long when displayed as a chart, so I want to shorten it by removing the year part.

2020-03-06 becomes 03/06

Following a great tutorial by Robin Wieruch, I now have a custom Hook to fetch data:

const useDataApi = (initialUrl, initialData) => {
  const [data, setData] = useState(initialData);
  const [url, setUrl] = useState(initialUrl);
  const [isLoading, setIsLoading] = useState(true);
  const [isError, setIsError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
      try {
        const response = await fetch(url);
        const data = await response.json();
        setData(data);
      } catch (error) {
        setIsError(true);
      }
      setIsLoading(false);
    };
    fetchData();
  }, [url]);
  return [{ data, isLoading, isError }];
}; 

Along with my charting component written in React Hooks:

const MyChart = () => {
  const [{ data, isLoading, isError }] = useDataApi(
    "https://some/api/domain",
    []
  );
  useEffect(() => {
    // I'm using useEffect to replace every date strings before rendering
    if (data) {
      data.forEach(
        d =>
          (d.reportDate = d.reportDate
            .replace(/-/g, "/")
            .replace(/^\d{4}\//g, ""))
      );
    }
  }, [data]);
  return (
    <>
      <h1>My Chart</h1>
      {isError && <div>Something went wrong</div>}
      {isLoading ? (
        . . .
      ) : (
        <>
        . . .
        <div className="line-chart">
          <MyLineChart data={data} x="reportDate" y="totalAmount" />
        </div>
        </>
      )}
    </>
  );
};                                   

The above works. But I have a feeling that this might not be the best practice because useEffect would be called twice during rendering. And when I try to adopt useReducer in my custom Hook, the code does not work anymore.

So I'm wondering what is the best way to edit data in this circumstance?

1 Answers1

2

You could create a mapping function for your data, which is then used by the hook. This can be done outside of your hook function.

const mapChartDataItem = (dataItem) => ({
  ...dataItem,
  reportDate: dataItem.reportDate.replace(/-/g, "/").replace(/^\d{4}\//g, ""))
});

The reportDate mapping is the same code as you have used in your useEffect.

Then in your hook function:

const data = await response.json();

// this is the new code
const mappedData = data.map(mapChartDataItem);

// change setData to use the new mapped data
setData(mappedData);

Doing it here means that you're only mapping your objects once (when they are fetched) rather than every time the value of data changes.


Update - with injecting the function to the hook

Your hook will now look like this:

const useDataApi = (initialUrl, initialData, transformFn) => {
  const [data, setData] = useState(initialData);
  const [url, setUrl] = useState(initialUrl);
  const [isLoading, setIsLoading] = useState(true);
  const [isError, setIsError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
      try {
        const response = await fetch(url);
        const data = await response.json();
        // if transformFn isn't provided, then just set the data as-is
        setData((transformFn && transformFn(data)) || data);
      } catch (error) {
        setIsError(true);
      }
      setIsLoading(false);
    };
    fetchData();
  }, [url, transformFn]);
  return [{ data, isLoading, isError }];
}; 

Then to call it, you can use the following:

const mapChartDataItem = (dataItem) => ({
  ...dataItem,
  reportDate: dataItem.reportDate.replace(/-/g, "/").replace(/^\d{4}\//g, ""))
});

const transformFn = useCallback(data => data.map(mapChartDataItem), []);

const [{ data, isLoading, isError }] = useDataApi(
  "https://some/api/domain",
  [],
  transformFn
);

For simplicity, because the transformFn argument is the last parameter to the function, then you can choose to call your hook without it, and it will just return the data as it was returned from the fetch call.

const [{ data, isLoading, isError }] = useDataApi(
  "https://some/api/domain",
  []
);

would work in the same was as it previously did.

Also, if you don't want (transformFn && transformFn(data)) || data in your code, you could give the transformFn a default value in your hook, more along the lines of:

const useDataApi = (
  initialUrl, 
  initialData, 
  transformFn = data => data) => {

  // then the rest of the hook in here

  // and call setData with the transformFn
  setData(transformFn(data));
}
Nick Howard
  • 171
  • 6
  • The `useDataApi` hook function should be kept generic since it needs to fetch data from other APIs with different date string formats as well. So the problem here really is how to do the replacing _after_ the data is fetched. Thanks for the mapping function. – George Huang Apr 07 '20 at 14:59
  • In that case, you could add an optional transform function as a parameter to your hook – Nick Howard Apr 07 '20 at 15:38
  • That should work. Can you provide an example in your answer? – George Huang Apr 08 '20 at 00:51
  • 1
    To use `transformFn` in `useEffect`, it has to be added to the dependency. But because it is a function, it will trigger infinite refetching. I think the easy solution is to wrap `transformFn` into a `useCallback` Hook. – George Huang Apr 08 '20 at 11:22
  • I've added `transformFn` to the dependency. Hope you don't mind. – George Huang Apr 10 '20 at 14:24