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>();
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.