I am using react's context to share data across component.
For example, I could create a user context:
const useFirebaseUser = () => {
const [user, setUser] = useState({} as User);
useEffect(() => {
return firebase.auth().onAuthStateChanged((user) => {
if (user) {
const { displayName, photoURL, uid } = user;
setUser({
displayName,
photoURL,
uid,
isAuthenticated: true,
} as User);
} else {
setUser({} as User);
}
});
}, []);
return user;
};
export const FirebaseUserContext = createContext({} as User);
export const GlobalFirebaseUserProvider = ({ children }: { children: ReactNode }) => (
<FirebaseUserContext.Provider value={useFirebaseUser()}>{children}</FirebaseUserContext.Provider>
);
similarly, I could also create a similar context to share other data, like todos
const useTodos = () => {
const [todos, setTodos] = useState(['']);
// ..
return { todos, setTodos };
};
export const TodosContext = createContext(
{} as { todos: string[]; setTodos: React.Dispatch<React.SetStateAction<string[]>> }
);
export const TodosContextProvider = ({ children }: { children: ReactNode }) => (
<TodosContext.Provider value={useTodos()}>{children}</TodosContext.Provider>
);
Upon these, I want to abstract out the value part. I am trying to create a Generic Provider:
import React, { createContext, ReactNode } from 'react';
export const CreateGenericContext = <T extends {}>(value: T) => {
const GenericContext = createContext({} as T);
const GenericContextProvider = ({ children }: { children: ReactNode }) => (
<GenericContext.Provider value={value}>{children}</GenericContext.Provider>
);
return { GenericContext, GenericContextProvider };
};
thus my user context could simplify into
export const {
GenericContext: UserContext,
GenericContextProvider: UserContextProvier,
} = CreateGenericContext(useUser());
However, React throw error message:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
Is this mean it is impossible to create a generic context providre for React? I had searched online, and tutorial seems show that for context not using hooks would work. However, in case of using react hooks, how to create a generic context provider in react?
Delay the custom hook, hook can only be called/invoked inside function component.
import React, { createContext, ReactNode } from 'react';
export const createGenericContext = <T extends {}>(hook: () => T) => {
const GenericContext = createContext({} as T);
const GenericContextProvider = ({ children }: { children: ReactNode }) => (
<GenericContext.Provider value={hook()}>{children}</GenericContext.Provider>
);
return { GenericContext, GenericContextProvider };
};
Related
I'm implementing a localStorage on NextJs TypeScript by following https://upmostly.com/next-js/using-localstorage-in-next-js but I get an error
on the context provider repeatedly.
Here is my code.
// My implementation for the context
import { useLocalStorage } from '#/Hooks/useLocalStorage';
import { Invoice, Invoices } from '#/Types/invoice';
import { createContext, Dispatch, SetStateAction, useContext } from 'react';
export const defaultInvoiceValue: Invoice = {
title: '',
items: [],
note: '',
status: '',
};
export const InvoiceContext = createContext<Invoices>({
invoices: [defaultInvoiceValue],
});
export const SetInvoicesContext = createContext<
Dispatch<SetStateAction<Invoices>>
>((value) => {
console.log('Set invoice context', value);
});
export const useInvoices = () =>
useLocalStorage<Invoices>('invoice', { invoices: [defaultInvoiceValue] });
export const useInvoiceContext = () => {
return useContext(InvoiceContext);
};
export const useSetInvoiceContext = () => {
return useContext(SetInvoicesContext);
};
// Provider wrapper
import { InvoiceContext, SetInvoicesContext, useInvoices } from '#/Context/InvoiceContext';
import { PropsWithChildren } from 'react';
export const InvoicesContextProvider = ({ children }: PropsWithChildren) => {
const [invoices, setInvoices] = useInvoices();
return (
<InvoiceContext.Provider value={invoices}>
<SetInvoicesContext.Provider value={setInvoices}>
{children}
</SetInvoicesContext.Provider>
</InvoiceContext.Provider>
)
};
The default context works fine. useSetInvoiceContext() also doesn't work
I would recommend you to check if the value provided in "invoices" in the hook useInvoinces is actually the same as the current value, before calling setInvoices. This will help you to avoid the infinite rendering.
For example, if the title and the items are the same as the current invoices, it will not update the state of invoices.
Something like:
let areNewInvoicesSameAsCurrent = newInvoices?.items?.every(invoice => currentInvoices.items.includes(invoice) );
if(newInvoices.title !== currentInvoices.title && !areNewInvoicesSameAsCurrent){
setInvoices(newInvoices)
}
I have a container that I want to connect to the redux store using redux connect function. The connect function typings seems to be able to determine the resulting props, but If I try to access the inferred props on the wrapped component I get typescript errors telling me that not the mapped actions nor the stats state are present on the intersection type connect generates:
Property 'actions' does not exist on type 'PropsWithChildren<Matching<{ stats: { getSessionsPending: boolean; getSessionsError: null; }; } & { actions: { setupApp: () => (dispatch: any, getState: any) => Promise<void>; getSessions: () => GetSessionThunk; dismissGetSessionsError: () => { ...; }; }; }, Matching<...> | (ClassAttributes<...> & Matching<...>)>>'.
Property 'actions' does not exist on type 'Matching<{ stats: { getSessionsPending: boolean; getSessionsError: null; }; } & { actions: { setupApp: () => (dispatch: any, getState: any) => Promise<void>; getSessions: () => GetSessionThunk; dismissGetSessionsError: () => { ...; }; }; }, Matching<...>> & { ...; }
This is a small reproduction example with a link to a typescript playground:
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
const setupApp = () => ({type: 'setup-ok'})
const Dashboard = () => <div>Hello</div>
function mapStateToProps(state:{stats: any}) {
return {
stats: state.stats,
};
}
const mapDispatchToProps = {
actions: { setupApp },
};
const connector = connect(mapStateToProps, mapDispatchToProps);
const DefaultPage = connector(props => {
useEffect(() => {
const { setupApp, getSessions } = props.actions;
setupApp().then(getSessions);
}, []);
const { sessions } = props.stats;
return <Dashboard sessions={sessions} />;
});
export default DefaultPage
Your use of mapDispatch here is actually incorrect and broken.
mapDispatch can either be an object containing action creators, or a function that receives dispatch as an argument. What you have there is neither of those - it's an object that has a nested object containing action creators, and that will not be handled correctly.
So, you either need to switch to doing const mapDispatch = {setupApp} and drop the actions nesting, or you need to write mapDispatch as a function and handle setting up the setupApp prop yourself.
You're right that we do recommend using the ConnectedProps<T> approach if you're using connect with TypeScript. However, we recommend using the React-Redux hooks API as the default today instead of connect, and one of the reasons is that it's way easier to use that with TS.
All that could be simplified down to:
const DefaultPage = () => {
const stats= useSelector( (state: RootState) => state.stats);
const dispatch = useDispatch();
useEffect(() => {
dispatch(setupApp());
}, [dispatch])
}
It seems that, for some reason, the types of the connect function doesn't play very well with actually passing a function taking the arguments it provides (don't ask me why)
However, it is possible to use the connector function (which is a bit alien to normal redux developers) to infer it's props type using a helper type from redux ConnectedProps so all you have to do is type your params as the result of applying ConnectedProps to your connector function:
import React, { useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux';
const setupApp = () => ({type: 'setup-ok'})
const Dashboard = () => <div>Hello</div>
function mapStateToProps(state:{stats: any}) {
return {
stats: state.stats,
};
}
const mapDispatchToProps = {
actions: { setupApp },
};
const connector = connect(mapStateToProps, mapDispatchToProps);
const DefaultPage = (props: ConnectedProps<typeof connector>) => {
useEffect(() => {
const { setupApp } = props.actions;
setupApp()
}, []);
const { sessions } = props.stats;
return <Dashboard sessions={sessions} />;
}
export default connector(DefaultPage)
AuthContext.tsx
import createDataContext from './createDataContext';
import serverApi from '../api/server';
const authReducer = ({state, action}: any) => {
switch(action.type){
default:
return state;
}
};
const signup = () => {
return async ({email, password}: any) => {
try{
const response = await serverApi.post('/signup', {email, password});
console.log(response.data)
}catch(err){
console.log(err.message);
}
};
}
const signin = ({dispatch}:any) => {
return ({email, password}: any) => { };
}
const signout = ({dispatch}: any) => {
return () => {};
}
export const {Provider, Context} = createDataContext(
authReducer,
{signin, signout, signup},
{isSignedIn: false}
);
createDataContext
import React, { useReducer } from 'react';
export default ({reducer, actions, defaultValue}: any) => {
const Context = React.createContext();
const Provider = ({ children }: any) => {
const [state, dispatch] = useReducer(reducer, defaultValue);
const boundActions: any = {};
for (let key in actions) {
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
}
I copy the code from a video tutorial where react native app has been developed with js extension. But the project I am working on has tsx extension i.e. TypeScript.
How to convert the above code so it will work in my typescript react native mobile app?
({reducer, actions, defaultValue}: any) is expecting one argument with three properties. But when you call it, you are passing three separate arguments. So you want (reducer: any, actions: any, defaultValue: any). Likewise, a reducer takes two arguments so you want authReducer = (state: any, action: any) =>, and so on for a bunch of your functions.
Now we want to get rid of all the any and use actual types! Some of those types we can import from react and others we will define ourselves.
The part that's tricky is getting your context to know the types for your specific action creators and what arguments each one requires. You want this so that you can get autocomplete suggestions for the actions and so you can know if you are calling them improperly. But that requires more advanced typescript like generics and mapped types so just copy and paste this and don't worry too much.
import React, { useReducer, FunctionComponent, Reducer, Dispatch } from 'react';
interface DataState {
isSignedIn: boolean;
// add any other properties here
}
interface SignInProps {
email: string;
password: string;
}
// you can change this
// it is common to use a type for `Action` that is a union of your specific actions
interface Action {
type: string;
payload: any;
}
// this is where I am getting tricky
type BoundActions<T> = {
[K in keyof T]: T[K] extends (d: Dispatch<Action>) => infer R ? R : never
}
type ContextValue<T> = {
state: DataState;
} & BoundActions<T>
export const createDataContext = <T extends {}>(reducer: Reducer<DataState, Action>, actions: T, defaultValue: DataState) => {
// context needs a defaultValue
const Context = React.createContext({state: defaultValue} as ContextValue<T>);
// type of children is known by assigning the type FunctionComponent to Provider
const Provider: FunctionComponent = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultValue);
const boundActions = {} as BoundActions<T>;
for (let key in actions) {
// #ts-ignore - I don't want to make a confusing mess so just ignore this
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
}
const authReducer = (state: DataState, action: Action): DataState => {
switch (action.type) {
default:
return state;
}
};
const signup = (dispatch: Dispatch<Action>) => {
return async ({ email, password }: SignInProps) => {
try {
const response = await serverApi.post('/signup', { email, password });
console.log(response.data)
} catch (err) {
console.log(err.message);
}
};
}
const signin = (dispatch: Dispatch<Action>) => {
return ({ email, password }: SignInProps) => { };
}
const signout = (dispatch: Dispatch<Action>) => {
return () => { };
}
export const { Provider, Context } = createDataContext(
authReducer,
{ signin, signout, signup },
{ isSignedIn: false }
);
The point of doing all that is to get intellisense and type checking when you consume the context.
import React, { useContext } from 'react';
import { Provider, Context } from .... // your path
const SampleComponent = () => {
// knows all of the properties available on the context
const {state, signin, signout, signup} = useContext(Context);
const handleClick = () => {
// knows that these need email and password
signin({email: '', password: ''});
signup({email: '', password: ''});
// knows that this one is ok to call with no args
signout();
}
return (
<div>{state.isSignedIn ? "Signed In!" : "Not Signed In"}</div>
)
}
const SampleApp = () => (
<Provider>
<SampleComponent/>
</Provider>
)
I found a blog where they did it as follows:
import { createContext } from 'react';
type ContextProps = {
alert: any,
showAlert: any
};
const alertContext = createContext<Partial<ContextProps>>({});
export default alertContext;
I created another file in which I set up the functions, like this:
const AlertState: SFC<AlertStateProps> = (props) => {
const initialState = {
alert: null
}
const [state, dispatch] = useReducer(alertReducer, initialState);
// Functions
const showAlert = (msg: any) => {
dispatch({
type: SHOW_ALERT,
payload: {
msg
}
});
// After 5 seconds clean error message
setTimeout(() => {
dispatch({
type: HIDE_ALERT
})
}, 5000);
}
return (
<alertContext.Provider
value={{
alert: state.alert,
showAlert
}}
>
{props.children}
</alertContext.Provider>
);
}
export default AlertState;
But, when I call alertContext in another file, like this:
const Login = (props) => {
const alertContext = useContext(AlertContext);
const { alert, showAlert } = alertContext;
console.log('context', alertContext);
...
}
In the console.log I can see that it takes the empty object and not the properties that are declared in the interface.
Someone knows what can I do?
First, Login component should be a child of AlertContext.Provider:
<AlertState>
<Login/>
</AlertState>
Then you need to pass the context object to createContext, you are passing AlertContext which is not defined.
// You should name context and React component with capital letter
const AlertContext = createContext<Partial<ContextProps>>({});
// Pass `AlertContext`
const {alert,showAlert} = useContext(AlertContext);
A quite common TypeScript error TS2349 that sth has no call signatures.
In different situations, it solves with different workarounds. But dealing with a react's Context the optimal solution I've found is using // #ts-ignore.
Please, what is the right approach in this case?
// auth.tsx
import { CurrentUserContext } from 'components/currentUser';
import React, { useState, useEffect, useContext } from 'react';
const Auth = ({ onLogin }: {onLogin: any}) => {
const [, setCurrentUserState] = useContext(CurrentUserContext);
const [credentials, { data }] = useMutation(AUTH);
...
useEffect(() => {
if (data) {
// TypeScript Error without #ts-ignore:
// This expression is not callable.
// Type '{}' has no call signatures. TS2349
// #ts-ignore
setCurrentUserState((state: any) => ({
...state,
currentUser: data.login.author,
}))
}
}, [data, setCurrentUserState]);
return <div>{data.login.author}</div>
}
// currentUser.tsx
import React, { createContext, useState } from 'react';
const CurrentUserContext = createContext([{}, () => {}]);
const CurrentUserProvider = ({ children }: {children: any}) => {
const [state, setState] = useState({
currentUser: null,
})
return (
<CurrentUserContext.Provider value={[state, setState]}>
{ children }
</CurrentUserContext.Provider>
)
};
export {
CurrentUserContext,
CurrentUserProvider,
}
n.b. "react" : "^16.13.1"
"typescript": "~3.7.2"
Describe and explicitly specify the types:
type IUserState = {
currentUser?: object;
};
type ICurrentUserContext = [IUserState, React.Dispatch<React.SetStateAction<IUserState>>];
const CurrentUserContext = React.createContext<ICurrentUserContext>([{}, () => null]);
const CurrentUserProvider = ({ children }: { children: any }) => {
const [state, setState] = useState<IUserState>({
currentUser: null,
});
return <CurrentUserContext.Provider value={[state, setState]}>{children}</CurrentUserContext.Provider>;
};