user10471818
user10471818

Reputation:

Object is possibly null in TypeScript React

With this code:

const Products = () => {
  const classes = useStyles();

  const { data } = useFetch('/categories');

  return (
    <div className={classes.root}>
      <GridList cellHeight={180} className={classes.gridList}>
        <GridListTile key="Subheader" cols={6} style={{ height: 'auto' }} />
        {data &&
          data.map((category: CategoryInterface) => {
            return (
              <GridListTile key={category.id}>
                <GridListTileBar title={category.name} />
              </GridListTile>
            );
          })}
      </GridList>
    </div>
  );
};

I get 'Object is possibly null' on data before the map, I tried using the && operator to get rid of it and I also tried defining a variable like const categories = data as CategoryInterface[] but that showed me I had to convert to unknown first, how should I do this instead?

Here is the useFetch hook

import { useEffect, useState } from 'react';

export const useFetch = (url: string) => {
  const [state, setState] = useState({ data: null, loading: true });

  useEffect(() => {
    setState(state => ({ data: state.data, loading: true }));

    fetch(url)
      .then(res => res.json())
      .then(json => setState({ data: json, loading: false }));
  }, [url, setState]);

  return state;
};

Upvotes: 5

Views: 8176

Answers (2)

ravibagul91
ravibagul91

Reputation: 20755

Your useFetch hook should be like this:

import { useEffect, useState } from 'react';

export const useFetch = (url: string) => {
  const [state, setState] = useState({ data: {}, loading: true }); //Don't assign null to state variable

  useEffect(() => {
    setState(state => ({ data: state.data, loading: true })); //Remove this line, this is not needed 

    fetch(url)
      .then(res => res.json())
      .then(json => setState({ data: json, loading: false }));
  }, [url, setState]); //Replace [url, setState] with [] as your second argument to make useEffect work for first time.

  return state;
};

And your usage should be:

const { data } = useFetch('/categories');

{data.length>0 &&
  data.map((category: CategoryInterface) => {
    return (
      <GridListTile key={category.id}>
        <GridListTileBar title={category.name} />
      </GridListTile>
    );
  })
}

Upvotes: 1

Fyodor Yemelyanenko
Fyodor Yemelyanenko

Reputation: 11848

Typescript assign types to JavaScript variables. Mostly when you use TS you should define variables types before usage. However TS sometimes can infer types of variables if possible.

It seems that you copy-paste JS code into TypeScript and trying to make it work. So first of all I suggest to define types for variables you're going to use, so TS will set correct types.

Initial call to useState makes state.data of type null (that is the only TS knows about it type). And unlike JS, TS doesn't allow to change type during execution. So state.data will have null type during program execution.

const [state, setState] = useState({ data: null, loading: true });
// Type of state is
// { data: null, loading: boolean }

To correct this you should provide type of your state variable ahead of time. One possible value of it is null. Another possible value - is data received from fetch. You probably should know what is type JSON structure of returned data from fetch so type it accordinately.

From your code I can guess, that data type will probably look like this

type IData = CategoryInterface[];

And CategoryInterface may look like

interface CategoryInterface {
    id: string;
    name: string; 
}

So IData will be second possible type of state.data. So assign type of state during useState call

const [state, setState] = useState<{data: IData | null, loading: boolean}>({ data: null, loading: true });

But you should leave in place {data && data.map (/*...*/)} as state.data will be undefined until data fully loaded.

Full code

interface CategoryInterface {
    id: string;
    name: string;
}

type IData = CategoryInterface[];

export const useFetch = (url: string) => {
    const [state, setState] = useState<{ data: IData | null, loading: boolean }>({ data: null, loading: true });

    useEffect(() => {
        setState(state => ({ data: state.data, loading: true }));

        fetch(url)
            .then(res => res.json())
            .then(json => setState({ data: json, loading: false }));
    }, [url, setState]);

    return state;
};

Upvotes: 2

Related Questions