Search code examples
reactjstypescriptreact-context

How to type properly object initial value in React CreateContext


How should I type properly in React Create Context initial value of constance trainsDetails which is an object with different properties. TrainsDetails is a single object fetched from end-point with values provided below in type TrainsDetailsResponseType:

type TrainsDetailsResponseType = {
    trainId: string;
    trainNo: string;
    lineId: string;
    route: string;
    lineKmPosition: string;
    speed: number;
    deviationFromTimetable: number;
    mileage: number;
    lastInspectionDate: string;
    nextInspectionDate: string;
    state: string;
};

Here is the interface of TrainsContextValues:

interface TrainsContextValues {
    trainsList: TrainsListResponseType[];
    trainsDetails: TrainsDetailsResponseType;
    handlePushToTrainsDetails: (arg: string) => void;
    trainId: string;
}

How should I type propely initial value of trainsDetails ?

export const TrainsContext = createContext<TrainsContextValues>({
    trainsList: [],
    trainsDetails: ,
    handlePushToTrainsDetails: () => undefined,
    trainId: '',
});

Here You can see also how I typed trainsDetails in useState hook

 const [trainsDetails, setTrainsDetails] = useState<TrainsDetailsResponseType>();

Solution

  • As you can see in your example, the state value type is TrainsDetailsResponseType | undefined:

    // typeof trainsDetails = TrainsDetailsResponseType | undefined
    const [trainsDetails, setTrainsDetails] = useState<TrainsDetailsResponseType>();
    

    You can specify a similar type for trainsDetails in the React context value:

    interface TrainsContextValues {
      trainsList: TrainsListResponseType[];
    
      // optional type (?) or TrainsDetailsResponseType | undefined
      trainsDetails?: TrainsDetailsResponseType;
    
      handlePushToTrainsDetails: (arg: string) => void;
      trainId: string;
    }
    
    export const TrainsContext = React.createContext<TrainsContextValues>({
      trainsList: [],
      trainsDetails: undefined,
      handlePushToTrainsDetails: () => undefined,
      trainId: "",
    });
    

    This is correct, since you really don't have the data for trainsDetails until you fetched it from end-point.


    UPDATED. LONG ANSWER

    It is important that the argument that you pass to createContext is not the initialValue of the context, it is the defaultValue. The defaultValue argument is only used when a component does not have a matching Provider above it in the tree.

    You have two scenarios for using context:

    1. - You allow the context to be used until all the necessary data is received. In this case, you should provide a fallback if there is no data:

    interface TrainsContextValues {
      trainsList: TrainsListResponseType[];
      trainsDetails?: TrainsDetailsResponseType;
      handlePushToTrainsDetails: (arg: string) => void;
      trainId: string;
    }
    
    export const TrainsContext = React.createContext<TrainsContextValues>({
      trainsList: [],
      trainsDetails: undefined,
      handlePushToTrainsDetails: () => {},
      trainId: "",
    });
    
    const Parent = () => {
      const [trainsDetails, setTrainsDetails] =
        useState<TrainsDetailsResponseType>();
    
      useEffect(() => {
        const getTrainsDetail = async () => {
          try {
            const response = await fetch("your-api");
            const data: TrainsDetailsResponseType = await response.json();
    
            setTrainsDetails(data);
          } catch {
            // your logic for the failed request
          }
        };
    
        getTrainsDetail();
      }, []);
    
      const contextValue = useMemo(
        () => ({
          // undefined | data from api
          trainsDetails,
    
          // replace to actually
          trainsList: [],
          handlePushToTrainsDetails: () => {},
          trainId: "",
        }),
        [trainsDetails]
      );
    
      return (
        <TrainsContext.Provider value={contextValue}>
          <Child />
        </TrainsContext.Provider>
      );
    };
    
    const Child = () => {
      const { trainsDetails } = useContext(TrainsContext);
    
      // technically, the component can be rendered out of context and it should be ready for this
      if (!trainsDetails) {
        return <FallbackComponent />;
      }
    
      return <TrainsDetailsComponent data={trainsDetails} />;
    };
    

    2. - You do not allow the use of context without real data. WARNING - this is a very strict mode, it is used only when the application cannot and should not use the context without real data:

    interface TrainsContextValues {
      trainsList: TrainsListResponseType[];
      trainsDetails: TrainsDetailsResponseType;
      handlePushToTrainsDetails: (arg: string) => void;
      trainId: string;
    }
    
    export const TrainsContext = React.createContext<TrainsContextValues | null>(
      null
    );
    
    const useTrainsContext = (): TrainsContextValues => {
      const data = useContext(TrainsContext);
    
      if (!data) {
        throw new Error("could not find trains context value");
      }
    
      return data;
    };
    
    const Parent = () => {
      const [trainsDetails, setTrainsDetails] =
        useState<TrainsDetailsResponseType>();
    
      useEffect(() => {
        const getTrainsDetail = async () => {
          try {
            const response = await fetch("your-api");
            const data: TrainsDetailsResponseType = await response.json();
    
            setTrainsDetails(data);
          } catch {
            // your logic for the dropped request
          }
        };
    
        getTrainsDetail();
      }, []);
    
      const contextValue = useMemo(
        () =>
          trainsDetails
            ? {
                trainsDetails,
                trainsList: [],
                handlePushToTrainsDetails: () => {},
                trainId: "",
              }
            : null,
        [trainsDetails]
      );
    
      if (!contextValue) {
        // some fallback for empty trainsDetails
        return <Loading />;
      }
    
      return (
        <TrainsContext.Provider value={contextValue}>
          <Child />
        </TrainsContext.Provider>
      );
    };
    
    const Child = () => {
      const { trainsDetails } = useTrainsContext();
    
      return <TrainsDetailsComponent data={trainsDetails} />;
    };
    

    The second scenario is specific and is used only when missing of data in the context is a logical error in the composition of components. And a user is not able to correct this situation. For example, missing of a store provider in redux.