I'm trying to add toast messages to my site using createContextand useContext from react to create a hook but when I use it I got an exception TypeError: addToast is not a function
import React, {
createContext, useContext, useCallback, useState,
} from 'react';
import { v4 as uuid } from 'uuid';
import Toast from '../components/Toast';
export interface ToastMessage {
id: string;
type?: 'success' | 'error' | 'default';
title: string;
description?: string;
}
interface ToastContextData {
addToast(message: Omit<ToastMessage, 'id'>): void;
removeToast(id: string): void;
}
const ToastContext = createContext<ToastContextData>({} as ToastContextData);
export const ToastProvider: React.FunctionComponent = ({ children }) => {
const [messages, setMessages] = useState<ToastMessage[]>([]);
const addToast = useCallback(
({ type, title, description }: Omit<ToastMessage, 'id'>) => {
const id = uuid();
const toast = {
id,
type,
title,
description,
};
setMessages((state) => [...state, toast]);
}, [],
);
const removeToast = useCallback((id: string) => {
setMessages((state) => state.filter((message) => message.id !== id));
}, []);
return (
<ToastContext.Provider value={{ addToast, removeToast }}>
{children}
<Toast messages={messages} />
</ToastContext.Provider>
);
};
export function useToast(): ToastContextData {
const context = useContext(ToastContext);
if (!context) {
throw new Error('use Toast must be used within a ToastProvider');
}
return context;
}
Using like this:
import { useToast } from '../../hooks/toast';
const { addToast } = useToast();
addToast({
type: 'error',
title: 'Problemo!',
description: 'That password and login doesn`t match. Try again?',
});
Obs: that code works perfectly in pure React (createReactApp) and on this project I'm using create next app
Related
I just finished working on a custom Context Provider and I came across a problem. The context isn't getting updated with the data from the function inside of it. It just stays the same (as specified default value).
I'm not really sure what the bug is :/
Here's the code:
useBreakpoints.tsx
// Functions
import { createContext } from 'react';
// Hooks
import { useContext, useEffect, useState } from 'react';
// Types
import type { ReactNode } from 'react';
type Width = number | undefined;
interface Breakpoints {
[key: number | string]: number;
}
interface Values {
[key: number | string]: boolean;
}
interface IProps {
children: ReactNode;
breakpoints: Breakpoints;
}
// Context
export const BreakpointsContext = createContext({});
export const BreakpointsProvider = ({ children, breakpoints }: IProps) => {
const [width, setWidth] = useState<Width>(undefined);
const [values, setValues] = useState<Values | undefined>(undefined);
useEffect(() => {
if (typeof window !== 'undefined') {
const handleResize = () => {
setWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
handleResize();
return () =>
window.removeEventListener('resize', () => {
handleResize();
});
}
}, []);
useEffect(() => {
if (width !== undefined) {
const handleValues = () => {
Object.keys(breakpoints).forEach((breakpoint, index) => {
setValues((prev) => ({
...prev,
[breakpoint]: width >= Object.values(breakpoints)[index],
}));
});
};
handleValues();
return () => window.removeEventListener('resize', handleValues);
}
}, [width]);
const exposed = {
width,
values,
};
return <BreakpointsContext.Provider value={exposed}>{children}</BreakpointsContext.Provider>;
};
export const useBreakpoints = () => useContext(BreakpointsContext);
export default BreakpointsProvider;
_app.tsx
// Providers
import { ThemeProvider } from 'next-themes';
import BreakpointsProvider from '../hooks/useBreakpoints';
// Variables
import { Style } from '#master/css';
// Styles
import '#master/normal.css';
import '../styles/master.css';
// Types
import { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
interface AppPropsWithLayout extends AppProps {
Component: NextPageWithLayout;
}
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return getLayout(
<ThemeProvider themes={Style.colorSchemes} attribute="class">
<BreakpointsProvider breakpoints={Style.breakpoints}>
<Component {...pageProps} />
</BreakpointsProvider>
</ThemeProvider>
);
}
export default MyApp;
Then I call it inside of a component like this
const test = useBreakpoints();
console.log(test);
I am working with NextJS, if that changes anything.
I have a container component:
import React, {FC, FormEvent, useState} from 'react';
// utils
import api from '../../api';
import {useAppSelector} from '../../store/hooks';
import CreateGroupPresentation from './CreateGroupPresentation';
interface Iprops {
close: () => void;
}
interface IformState {
title: string;
upcs: string;
}
export interface IhandleChangeArgs {
currentTarget: { value: string, id: string };
}
const CreateGroup: FC<Iprops> = ({close}) => {
// get auth from redux
const {token} = useAppSelector(state => state.auth);
// local state for form
const initialState: IformState = {title: '', upcs: ''};
const [formState, setFormState] = useState(initialState);
const {title, upcs} = formState;
// handle change and submit for form
const handleChange = (event: IhandleChangeArgs) => {
const {id, value} = event.currentTarget;
// prevents non-digits from being entered into the upc input
if (id === 'upcs') {
const numbers = /[\d\s]*/;
const total = value.split('');
const newChar = total[total.length - 1];
if (!numbers.test(newChar)) return;
}
setFormState({...formState, [id]: value});
};
const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
// converts the string from the upcs textarea to an array of numbers to send to the api
const upcsToNumberArray: number[] = [];
upcs
.trim()
.split('\n')
.forEach(upc => upcsToNumberArray.push(parseInt(upc)));
// eliminates duplicate UPCs
const noDupes = [...new Set(upcsToNumberArray)];
// send to api
try {
if (token) {
const response = await api.createGroup(token, {
title,
upcs: noDupes,
});
console.log(response);
}
} catch (error: any) {
console.log(error);
}
};
const presentationProps = {handleSubmit, handleChange, title, upcs, close};
return <CreateGroupPresentation {...presentationProps} />;
};
export default CreateGroup;
...which passes several props to a presentational component as presentationProps:
import {Modal, View} from 'react-native';
import React, {FC, FormEvent} from 'react';
import styles from './CreateGroup.scss';
import MyButton from '../shared/MyButton/MyButton';
import LabelInput from '../shared/LabelInput/LabelInput';
import {IhandleChangeArgs} from './CreateGroup';
interface Iprops {
handleSubmit: (event: FormEvent) => Promise<void>;
handleChange: (event: IhandleChangeArgs) => void;
title: string;
upcs: string;
close: () => void;
}
const CreateGroup: FC<Iprops> = ({handleChange, title, upcs, close}) => {
return (
<Modal animationType="slide" transparent>
<View style={styles['modal-back']}>
<View style={styles['modal-fore']}>
<LabelInput
label="Title"
value={title}
onChangeText={text =>
handleChange({
currentTarget: {
value: text.valueOf().toString(),
id: 'title',
},
})
}
/>
<LabelInput
label="UPCs"
value={upcs}
multiline
numberOfLines={12}
onChangeText={text =>
handleChange({
currentTarget: {value: text.valueOf().toString(), id: 'upcs'},
})
}
/>
<MyButton onPress={close} text="close" />
</View>
</View>
</Modal>
);
};
export default CreateGroup;
...but I get a render error message that handleChange is not a function and is in fact undefined. How can I pass the function correctly? Is there a way that I can pass it without changing the function body itself? I am trying to keep the container component the same in the react-native version of my app as it is in the web version.
note: the handleSubmit function is not currently used in the presentational component, but I will handle that next.
I think you forgot to rename the presentational component in the second file from CreateGroup to CreateGroupPresentation. Right now it is called CreateGroup in both files. I.e.
....
const CreateGroupPresentation: FC<Iprops> = ({
handleChange,
title,
upcs,
close,
}) => {
...
};
...
export default CreateGroupPresentation;
Otherwise it looks fine.
I just finished a small project and I tried to publish it on Vercel, the project works without errors locally, but on the server I get the error: "Cannot read properties of undefined (reading 'notes')", apparently not It recognizes the state that I have in my application, this would be my useNotes file:
import { useContext } from "react";
import { NotesContext } from "../context/NotesContext";
export const useNotes = () => {
const { notestate, toggleNote, addNote, changeState, handleDeleteNote } = useContext(NotesContext);
const { notes } = notestate;
return {
noteGeneral: notestate,
notes: notes,
notinteresting: notes?.filter(note => !note?.interesting).length,
interesting: notes?.filter(note => note?.interesting === true).length,
lisInteresting: notes?.filter(note => note?.interesting === true),
listNotInteresting: notes?.filter(note => note?.interesting === false),
toggleNote,
addNote,
changeState,
handleDeleteNote
}
}
this would be the context of my application:
import { createContext } from "react";
import { Note, NoteState } from "../interfaces/interfaces";
export type NoteContextProps = {
notestate: NoteState,
toggleNote: (id: number) => void;
addNote: (note: Note) => void;
changeState: (action: string) => void;
handleDeleteNote: (id: number) => void;
}
export const NotesContext = createContext<NoteContextProps>({} as NoteContextProps);
and this is the provider of my application
import { useEffect, useReducer } from 'react';
import { Note, NoteState } from '../interfaces/interfaces';
import { NoteReducer } from './NoteReducer';
import { NotesContext } from './NotesContext';
import { DateNote } from '../../helpers/DateNote';
const { today } = DateNote();
const INITIAL_STATE: NoteState = {
notesCount: 2,
notes: [
{
id: 1,
description: 'Welcome to NotesApp, here you can write all the notes you want. If you liked the project leave a comment on my social networks :)',
interesting: true,
title: 'Hello there',
created: today
},
],
active: 'All',
}
interface props {
children: JSX.Element | JSX.Element[]
}
const NotesProvider = ({ children }: props) => {
const localData = localStorage.getItem('notes');
const toggleNote = (id: number) => {
dispatch({ type: 'toggleInteresting', payload: { id } })
}
const addNote = (note: Note) => {
dispatch({ type: 'addNote', payload: note })
}
const changeState = (active: string) => {
dispatch({ type: 'changeState', payload: active })
}
const handleDeleteNote = (id: number) => {
dispatch({ type: 'deleteNote', payload: { id } })
}
const [notestate, dispatch] = useReducer(NoteReducer, INITIAL_STATE, () => {
return localData ? JSON.parse(localData) : localStorage.setItem('notes', JSON.stringify(INITIAL_STATE))
});
useEffect(() => {
localStorage.setItem('notes', JSON.stringify(notestate))
}, [notestate])
return (
<NotesContext.Provider value={{ toggleNote, addNote, changeState, handleDeleteNote, notestate }}>
{children}
</NotesContext.Provider>
)
}
export default NotesProvider;
I am storing the state of my application in Localstorage.
I've been looking for the solution for more than two days but I can't find it
const [notestate, dispatch] = useReducer(NoteReducer, INITIAL_STATE, () => {
return localData ? JSON.parse(localData) : localStorage.setItem('notes', JSON.stringify(INITIAL_STATE))
});
I think here when there is no local data, the notestate receives value from localStorage.setItem(), which is undefined, so the notestate will be undefined.
I'm working on implementing TypeScript on a small codebase I've been working on and having a few troubles with the above error. I've searched for answers but none seemed to fix the actual issue I was having.
I'm getting the error:
Type 'ContextType | null' is not assignable to type 'ContextType'.
I have tried setting it to null, undefined, and object, and nothing seems to help so I'd appreciate some help with this one!
Store.txt
import { useReducer, createContext, useMemo } from "react";
import { INITIAL_DATA } from './todo/constants';
import { ContextType, ITodo, ACTIONTYPE } from './todo/models';
export const StoreContext = createContext<ContextType | null>(null)
export enum ACTIONS {
DELETE_TODO = "delete_todo",
ADD_TODO = "add_todo",
};
const reducer = (state: ITodo, action: ACTIONTYPE) => {
switch (action.type) {
case ACTIONS.DELETE_TODO:
return [...action.payload];
case ACTIONS.ADD_TODO:
return action.payload;
default:
return state;
}
};
interface Props {
children: React.ReactNode;
}
export const StoreProvider = ({ children }: Props) => {
const [state, dispatch] = useReducer(reducer, INITIAL_DATA);
const contextValue: ContextType = useMemo(() => {
return { state, dispatch };
}, [state, dispatch]);
return (
<StoreContext.Provider value={contextValue}>
{children}
</StoreContext.Provider>
);
};
Component.tsx
import { useContext } from 'react';
import { StoreContext, ACTIONS } from '../../../store';
import { ContextType } from '../../models'
export const AddTodo = () => {
const { state, dispatch }: ContextType = useContext(StoreContext);
const validateFirstStep = async () => {
return await isValid(firstStepForm);
}
const closeDrawer = () => {
onClose();
}
const handleSubmit = async () => {
const newTodoEntry = { /** todo **/ }
const newData = [...state, newTodoEntry];
dispatch({ type: ACTIONS.ADD_TODO, payload: newData });
}
return (
<div>
{ /* Something Todo happens here */ }
</div>
)
}
Models.tsx
import { ACTIONS } from '../../store';
export type ITodos = {
id: string;
todoName: string;
todoType: string;
}[];
export type ContextType = {
state: ITodos;
dispatch: React.Dispatch<ACTIONTYPE>;
}
export type ACTIONTYPE =
| { type: ACTIONS.ADD_TODO, payload: ITodos }
| { type: ACTIONS.DELETE_TODO; payload: ITodos }
You need to provide default context value in case of there is no provider higher in the react tree;
export const StoreContext = createContext<ContextType>({todos: [], dispatch: () => {}})
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>
)