I have only been using TypeScript a couple months, and I just noticed that the compiler is not enforcing the shape of data a function accepts if that function is accessed through React.useContext().
This setup here is not exactly what I have going on, but it more or less shows the problem I am trying to figure out.
import * as React from 'react';
//==>> Reducer State Interface
interface InitialStateInterface {
handleSettingFooBar: Function;
foo: string | null;
bar: boolean;
}
//==>> Function Interface
interface PayloadInterface {
foo?: string | null;
bar?: boolean;
}
//==> Reducer
interface ReducerInterface {
type: string;
payload: PayloadInterface;
}
enum ActionTypes {
SET_FOO = 'SET_FOO',
SET_BAR = 'SET_BAR',
}
const initialState: InitialStateInterface = {
handleSettingFooBar: () => null,
foo: null,
bar: false,
};
const SomeContext = React.createContext<InitialStateInterface>(initialState);
const ContextReducer = (state: any, { type, payload }: ReducerInterface) => {
switch (type) {
case ActionTypes.SET_FOO:
return { ...state, foo: payload.foo };
case ActionTypes.SET_BAR:
return { ...state, bar: payload.bar };
default:
return state;
}
};
const SomeProvider = () => {
const [state, dispatch] = React.useReducer(ContextReducer, initialState);
function handleSettingFooBar(data: PayloadInterface) {
let { foo, bar } = data;
if (foo) dispatch({ type: ActionTypes.SET_FOO, payload: { foo } });
if (bar) dispatch({ type: ActionTypes.SET_BAR, payload: { bar } });
}
/** Okay, of course, no error */
handleSettingFooBar({ foo: 'test' });
/** Error as expected, type does not match */
handleSettingFooBar({ foo: false });
/** Error as expected, key does not exist */
handleSettingFooBar({ randomKey: 'cant do this' });
return <SomeContext.Provider value={{ ...state, handleSettingFooBar }} />;
};
/* ===> But when writing a component that uses that context <=== */
export const SomeComponent = () => {
const { handleSettingFooBar } = React.useContext(SomeContext);
/** Why is the compiler not yelling at me here??? */
handleSettingFooBar({ randomKey: 'hahaha' });
};
export { SomeProvider, SomeContext };
I have tried putting the interface in when calling the context, like this:
const { handleSettingFooBar } = React.useContext<InitialStateInterface>(SomeContext);
But that made no difference.
I am expecting that if somebody is authoring a component that uses this context and its provided functions, that it will regulate the data (at compile time, of course) they try to pass in so a generic setter may not add a value that does not belong in the context reducer state.
Please help, thanks!
The SomeContext has the InitialStateInterface type which defines handleSettingFooBar as handleSettingFooBar: Function, and it does not know how you actually implemented it.
You can change that to handleSettingFooBar: (data:PayloadInterface) => void and then the typescript would know what kind of input should be allowed for it.
Related
Apologies for the somewhat opaque title, but I am having difficulties being more precise here.
So I have a Context/Reducer Logic, where I initialise the context with some values. I then have a reducer Logic on a custom Provider and use useMemo to calculate values. When trying to access one on of those values (that isn't in the state/initialState) on a component typescript gets angry at me and tells me that said value does not exist on State. What is the best way to remedy this warning?
I have the following definition of a Context/Reducer.
interface State {
displaySidebar: boolean
}
const initialState = {
displaySidebar: false
}
type Action =
| {
type: 'OPEN_SIDEBAR'
}
| {
type: 'CLOSE_SIDEBAR'
}
const UIContext = React.createContext<State>(initialState)
UIContext.displayName = 'UIContext'
const uiReducer = (state: State, action: Action): State => {
switch (action.type) {
case 'OPEN_SIDEBAR': {
return {
...state,
displaySidebar: true,
}
}
case 'CLOSE_SIDEBAR': {
return {
...state,
displaySidebar: false,
}
}
}
}
const UIProvider: FC = (props) => {
const [state, dispatch] = React.useReducer(uiReducer, initialState)
const openSidebar = (): void => dispatch({ type: 'OPEN_SIDEBAR' })
const closeSidebar = (): void => dispatch({ type: 'CLOSE_SIDEBAR' })
const value = useMemo(
() => ({
...state,
openSidebar,
closeSidebar,
}),
[state]
)
return <UIContext.Provider value={value} {...props} />
}
export const useUI = () => {
const context = React.useContext(UIContext)
if (context === undefined) {
throw new Error(`useUI must be used within a UIProvider`)
}
return context
}
export const ManagedUIContext: FC = ({ children }) => (
<UIProvider>
<ThemeProvider>{children}</ThemeProvider>
</UIProvider>
)
now when I try to use const {closeSidebar} = useUI() in a component typescript gets angry with me and tells me that Property 'closeSidebar' does not exist on type 'State'. I get that, but I was not able to figure out how to properly add closeSidebar to the React.Context type.
When you create context you tell TS that its type will be State, so it doesn't expect anything else to be there. If you want to add additional fields you can create an intersection type, state + methods, either as a named type of just React.createContext<State & {openSidebar : ()=> void, closeSidebar: ()=> void}>. Note that as your initial state doesn't have methods you either need to make them optional or provide some sort of dummy versions.
I am trying to use Typescript with Redux, and design them according to Redux Ducks. I am struggling with this typecheck here and I cannot figure out why.
const UPDATE = "ticTacToe/board/update";
const RESET = "ticTacToe/board/reset";
export default function reducer(
state = initialState,
action: IBoardActions
): IBoardState {
switch (action.type) {
case UPDATE:
const { nextPlayer, clickedIndex } = action;
// TypeScript error: Property 'nextPlayer' does not exist on type
//'{ type: string; nextPlayer: string; clickedIndex: number; } | { type: string; }'. TS2339
const newSquares = [...state.squares];
newSquares[clickedIndex] = nextPlayer;
return {
squares: newSquares,
oIsNext: !state.oIsNext
};
case RESET:
return initialState
default:
return state;
}
}
type BoardAction = typeof updateBoard | typeof resetBoard;
export type IBoardActions = ReturnType<BoardAction>;
export function updateBoard(nextPlayer: string, clickedIndex: number) {
return {
type: UPDATE,
nextPlayer,
clickedIndex
};
}
export function resetBoard() {
return {
type: RESET
};
}
I have tried to fix it with enum, but this doesn't work as well. Is that the problem of switch?
enum Action {
UPDATE = 'update',
RESET = 'reset'
}
export default function reducer(
state = initialState,
action: IBoardActions
): IBoardState {
switch (action.type) {
case Action.UPDATE:
const { nextPlayer, clickedIndex } = action;
//TypeScript error: Property 'nextPlayer' does not exist on type '{ type: Ac
tion; nextPlayer: TicTacToeSymbols; clickedIndex: number; } | { type: Acti
on; }'. TS2339
const newSquares = [...state.squares];
newSquares[clickedIndex] = nextPlayer;
return {
squares: newSquares,
oIsNext: !state.oIsNext
};
case Action.RESET:
return initialState
default:
return state;
}
}
type BoardAction = typeof updateBoard | typeof resetBoard;
export type IBoardActions = ReturnType<BoardAction>;
export function updateBoard(nextPlayer: TicTacToeSymbols, clickedIndex: number) {
return {
type: Action.UPDATE,
nextPlayer,
clickedIndex
};
}
export function resetBoard() {
return {
type: Action.RESET
};
}
Why am I getting this error? It seems like my typing is correct based on the error message.
You're looking for discriminated unions. Note that the type of IBoardActions in your snippet is-
{ type: string; nextPlayer: string; clickedIndex: number; } | { type: string; }
But in reality, you want-
{ type: typeof UPDATE; nextPlayer: string; clickedIndex: number; } | { type: typeof RESET; }
This speaks explicitly that the first alternative is only used when the type property has the same type as that of UPDATE, and similarly for RESET.
But in your case, both UPDATE and RESET are of type string. There's no way to discriminate them.
This is where string literal types come in, change your definition of UPDATE and RESET to-
const UPDATE = "ticTacToe/board/update" as const;
const RESET = "ticTacToe/board/reset" as const;
The as const syntax is known as const assertions. If you notice, the type of UPDATE has now become a string literal set to its value, same for RESET.
Here's a playground demo
You can also use interface like this.
export interface IBoardActions extends ReturnType<typeof updateBoard>, ReturnType<typeof resetBoard>
We don't really encourage the use of discriminated unions any more as they have their own set of problems and require writing a lot of extra code.
The official recommendation is to use the official redux toolkit which has TypeScript support in mind and will reduce your code drastically. Please see the official redux tutorial on modern redux for a short introduction and then take a look at the essentials tutorial for more in-depth information.
I'm facing an error:
react-dom.development.js:23093 Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
I understand that the problem may be due to the fact that I'm calling the checkError and validationPassed functions that modify the state through the useReducer, within the checkValidations function that is called through the useEffect hook, but I don't know how to solve it
The code is as:
interface ValidationField {
errorMessage?: string;
focused: boolean;
hasError: boolean;
}
interface ClientEditorState {
client: Client;
validations: { [key in keyof Client]: ValidationField };
}
enum clientEditorActions {
UPDATE_ENTITY = 'CLIENT_EDITOR/UPDATE_ENTITY',
UPDATE_FOCUSED = 'CLIENT_EDITOR/UPDATE_FOCUSED',
VALIDATION_ERROR = 'CLIENT_EDITOR/VALIDATION_ERROR',
VALIDATION_PASSED = 'CLIENT_EDITOR/VALIDATION_PASSED',
}
interface UpdateEntityAction extends Action<typeof clientEditorActions.UPDATE_ENTITY> {
name: string;
value: string | boolean;
}
interface UpdateFocusedAction extends Action<typeof clientEditorActions.UPDATE_FOCUSED> {
name: string;
}
interface ValidationErrorAction extends Action<typeof clientEditorActions.VALIDATION_ERROR> {
message: string;
name: string;
}
interface ValidationPassedAction extends Action<typeof clientEditorActions.VALIDATION_PASSED> {
name: string;
}
type ClientEditorActions = UpdateEntityAction | UpdateFocusedAction | ValidationErrorAction | ValidationPassedAction;
const clientReducer: Reducer<ClientEditorState, ClientEditorActions> = (prevState, action) => {
switch (action.type) {
case clientEditorActions.UPDATE_ENTITY:
const clientUpdated = _cloneDeep(prevState || ({} as Client));
_set(clientUpdated, `client.${action.name}`, action.value);
return clientUpdated;
case clientEditorActions.UPDATE_FOCUSED:
const validationField = _cloneDeep(prevState);
_set(validationField, `validations.${action.name}.focused`, true);
return validationField;
case clientEditorActions.VALIDATION_ERROR:
const errorField = _cloneDeep(prevState);
_set(errorField, `validations.${action.name}.hasError`, true);
_set(errorField, `validations.${action.name}.errorMessage`, action.message);
return errorField;
case clientEditorActions.VALIDATION_PASSED:
const passed = _cloneDeep(prevState);
_set(passed, `validations.${action.name}.hasError`, false);
_set(passed, `validations.${action.name}.errorMessage`, undefined);
return passed;
default:
return prevState;
}
};
...
const getInitialState = (): ClientEditorState => ({
client: entity as Client,
validations: {
firstName: {
focused: false,
hasError: false,
},
},
});
const [state, clientDispatch] = useReducer(clientReducer, getInitialState());
const checkError = useCallback((name: string, message: string) => {
clientDispatch({
type: clientEditorActions.VALIDATION_ERROR,
name,
message,
});
}, []);
const validationPassed = useCallback((name: string) => {
clientDispatch({
type: clientEditorActions.VALIDATION_PASSED,
name,
});
}, []);
const checkValidations = useCallback(
(c: Client) => {
let validation = false;
const { firstName } = state.validations;
if (!c.firstName && firstName.focused) {
validation = false;
checkError('firstName', f('client.requiredFieldClient'));
} else {
validation = true;
validationPassed('firstName');
}
},
[checkError, f, state.validations, validationPassed],
);
const [clientUpdateHandler] = useDebouncedCallback((clientUpdated: Client) => {
dispatch(updateEntityEditor(clientUpdated));
}, 800);
useEffect(() => {
if (!_isEqual(state.client, entity)) {
clientUpdateHandler(state.client as Client);
}
const { firstName } = state.validations;
if (firstName.focused) checkValidations(state.client);
}, [checkValidations, clientUpdateHandler, entity, state.client, state.validations]);
I understand that the problem may be due to the fact that I'm calling the checkError and validationPassed functions that modify the state through the useReducer, within the checkValidations function that is called through the useEffect hook
Yeah, exactly. Try to reduce your dependencies in the useEffect and the associated functions in useCallback. Any one of these changing will cause the useEffect to rerun.
Back to the question, your useEffect currently depends on state.validations. So whenever state.validations changes the useEffect will rerun. Consider doing
const { firstName } = state.validations;
outside the useEffect and the checkValidations callback. This will stop it from rerunning every time state.validations changes.
I am trying to use react context and I'm not sure how to use it correctly with typescript.
interface ITodoContext {
todos: Todo[] | null;
currentlyEditingTodoID: number|null;
editTodo(todo: Todo): () => void
}
export const TodoContext = React.createContext<ITodoContext>({
todos: null,
// how do put the editTodo() ?
});
The editTodo method will set currentlyEditingTodoID to the todo.id, and it might show a modal or something if its not null.
However, I'm not sure how to define the method correctly.
Here is how you should define your type and then your implementation:
interface ITodoContext {
todos: Todo[] | null;
currentlyEditingTodoID: number | null;
editTodo: (todo: Todo) => void;
}
export const TodoContext = React.createContext<ITodoContext>({
todos: null,
currentlyEditingTodoID: null,
editTodo(todo) {
// your implementation of the function
},
});
You can also use arrow notation for the function :
export const TodoContext = React.createContext<ITodoContext>({
todos: null,
currentlyEditingTodoID: null,
editTodo: (todo) => {
// your implementation of the function
},
});
The right way to define methods is:
editTodo: (todo: Todo) => void
I'm learning redux with typescript.
Until I hit combineReducer,
export const reducerBase= combineReducers({
stateA: reducerA,
stateB: reducerB,
stateC: reducerC
});
is working fine, but I can't manage to hand write it.
currently what I'm trying to do is
function initialState(): BaseState{
return {
a: [],
b: [],
c: []
};
}
type AllActions = AAction | BAction | CAction;
// tslint:disable-next-line:no-any
function isAAction(arg: any) {
return arg.includes('_A');
}
export function reducerBase(
state: BaseState= initialState(),
action: AllActions
): BaseState{
if (isAAction(action.type)) {
return { ...state, a: reducerA(state.a, action) }; // here the problem
}
return state;
}
type AllActions is not assignable to type AAction,
How should I proceed?
It's working,
export function reducerBase(
state: BaseState= initialState(),
action: AllActions
): BaseState{
a: reducerA(state.a, action as AAction);
b: reducerB(state.b, action as BAction);
c: reducerC(state.c, action as CAction);
return {a,b,c};
}
while BAction is called on reducerA and reducerC, they do nothing and just returned their own state.
The problem is because isAAction function just returns boolean, there is no way Typescript could know that this boolean indicates action type.
First solution - just cast it to AAction:
return { ...state, a: reducerA(state.a, action as AAction) };
Second solution - change isAAction method into typeguard, then Typescript will know that this function returns true when AAction is passed as argument
function isAAction(action: any): arg is AAction {
return action.type.includes('_A');
}
// ...
if (isAAction(action)) {
return { ...state, a: reducerA(state.a, action) };
}
EDIT: Reffering to your comment. I personally wouldn't use reducerBase reducer at all and just used the following structure. Action type will determine which reducer should react on given action.
const reducers = {
stateA: reducerA,
stateB: reducerB,
stateC: reducerC
};
export const store = createStore(
combineReducers(reducers)
);
export function reducerA(state = new StateA(), action: AAction) {
switch (action.type) {
case AActionType.SOME_ACTION: return {
...state,
// some changes based on action
}
case AActionType.OTHER_ACTION: return {
...state,
// some changes based on action
}
default: return state;
}
}