React Context value doesn't change - reactjs

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.

Related

Property 'Provider' does not exist on type '() => string'.ts(2339)

when i export and utilize my context I am getting un error as :
Property 'Provider' does not exist on type '() => string'.ts(2339).
the idea here is to used the context as a service where it's required. so the context function placed separately and imported where it needs. But throw the error.
any one highlight me the missed part please?
here is my context:
import { createContext, useContext } from "react";
export const TabContext = <A extends unknown | null>() => {
const useTabsActionContext = createContext<A | undefined>(undefined);
const useTabsContext = () => {
const ctx = useContext(useTabsActionContext);
if (ctx === undefined) {
throw new Error("invalid");
}
return ctx;
};
return [useTabsActionContext, useTabsContext] as const;
};
when I use:
import { Dispatch, ReactNode, SetStateAction } from "react";
import { TabContext } from "./tabContext";
export type ActiveTab = string;
export type SetActiveTab = Dispatch<SetStateAction<ActiveTab>>;
const [useTabsActionContext, TabsActionContext] = TabContext<ActiveTab>();
const [useTabsContext, TabsContext] = TabContext<SetActiveTab>();
export { useTabsActionContext, useTabsContext };
export interface TabsProps {
children: ReactNode;
}
function Tabs(props: TabsProps) {
const { children } = props;
return (
<TabsActionContext.Provider> //error
<TabsContext.Provider>{children}</TabsContext.Provider> //error
</TabsActionContext.Provider>
);
}
export default Tabs;
Live Demo
as per Henrik, when i swapped the value, it works fine.
import { createContext, useContext } from "react";
export const TabContext = <A extends unknown | null>() => {
const useTabsActionContext = createContext<A | undefined>(undefined);
const useTabsContext = () => {
const ctx = useContext(useTabsActionContext);
if (ctx === undefined) {
throw new Error("invalid");
}
return ctx;
};
return [useTabsContext, useTabsActionContext] as const;
};
thanks

Pass useRef() to grandchild component using typescript

Example code can be found below...
(Parent) App.tsx
import React, { useRef } from 'react';
import { Switch } from "react-router-dom";
import axios, { AxiosResponse } from 'axios';
import { AdminRoute } from './auth/protectedRoutes';
import Home from './views/Home';
export interface Data {
id: number;
name: string;
}
function App(): JSX.Element {
// variables
const searchDebouncerRef = useRef(false);
const [data, setData] = useRef<Array<Data>>([]);
// functions
async function updateData(searchString?: string | null) {
try {
const response: AxiosResponse<Array<Data>> = searchString
? await axios.get(`https://example.com/Api/Data$filter=contains(name, ${searchString})`)
: await axios.get('https://example.com/Api/Data');
if (searchDebouncerRef.current) {
return;
}
setData(response.data);
catch (e) {
console.log(e);
}
}
async function initData() {
try {
await updateData();
catch (e) {
console.log(e);
}
}
// setup
useEffect(() => {
initData();
}, []);
// render
return (
<>
<Switch>
<AdminRoute path="/">
<Home ref={searchDebouncerRef} updateData={updateData} data={data} />
</AdminRoute>
</Switch>
</>
)
}
export App;
(Child) Home.tsx
import React, { forwardRef } from 'react';
import { Data as DataRow } from '../App';
import Search from '../components/Search';
interface PROPS {
updateData: (searchString?: string | null) => void;
data: Array<DataRow>;
}
const Home: React.FC<any> = forwardRef(
({ updateData, data }: PROPS, ref) => {
return (
<div>
<Search isSearchDebouncingRef={ref} updateData={updateData} />
{data.map((row: DataRow) => ({
<p key={row.id}>{row.name}</p>
}))}
</div>
);
}
);
(Grandchild) Search.tsx
import React, { ChangeEvent, useCallback, useState } from 'react';
import { debounce } from 'lodash';
interface PROPS {
updateData: (searchString?: string | null) => void;
isSearchDebouncingRef: ???? // <-----------------------------------------------------
}
function Search({ updateData, isSearchDebouncingRef }: PROPS): JSX.Element {
// variables
const [searchText, setSearchText] = useState('');
const [searchDebouncerHasCompleted, setSearchDebouncerHasCompleted] = useState(false);
const searchDebouncer = useCallback(
debounce(() => {
setSearchDebouncerHasCompleted(true);
isSearchDebouncingRef.current = false;
}, 3000),
[]
);
// functions
function handleSearch(event: ChangeEvent<HTMLInputElement>) {
setSearchText(event.target.value);
isSearchDebouncingRef.current = true;
searchDebouncer();
}
// setup
useEffect(() => {
if (searchDebouncerHasCompleted) {
setSearchDebouncerHasCompleted(false);
updateData(searchText || null);
}
}, [searchDebouncerHasCompleted]);
// render
return <input type="text" value={searchText} onChange={(e) => handleSearch(e)} />; // ToDo: add icons(search, spinner, clear)
}
The grandchild file is where I am having trouble figuring out how to identify the type for the ref(see interface PROPS). Also the child file I would like to replace any, with the proper type if possible.

How to test React.js page that uses Context and useEffect?

I'm having trouble testing a page that has Context and useEffect using Jest and Testing-library, can you help me?
REPO: https://github.com/jefferson1104/padawan
My Context: src/context/personContext.tsx
import { createContext, ReactNode, useState } from 'react'
import { useRouter } from 'next/router'
import { api } from '../services/api'
type PersonData = {
name?: string
avatar?: string
}
type PersonProviderProps = {
children: ReactNode
}
type PersonContextData = {
person: PersonData
loading: boolean
handlePerson: () => void
}
export const PersonContext = createContext({} as PersonContextData)
export function PersonProvider({ children }: PersonProviderProps) {
const [person, setPerson] = useState<PersonData>({})
const [loading, setLoading] = useState(false)
const router = useRouter()
function checkAvatar(name: string): string {
return name === 'Darth Vader'
? '/img/darth-vader.png'
: '/img/luke-skywalker.png'
}
async function handlePerson() {
setLoading(true)
const promise1 = api.get('/1')
const promise2 = api.get('/4')
Promise.race([promise1, promise2]).then(function (values) {
const data = {
name: values.data.name,
avatar: checkAvatar(values.data.name)
}
setPerson(data)
setLoading(false)
router.push('/battlefield')
})
}
return (
<PersonContext.Provider value={{ person, handlePerson, loading }}>
{children}
</PersonContext.Provider>
)
}
My Page: src/pages/battlefield.tsx
import { useContext, useEffect } from 'react'
import { useRouter } from 'next/router'
import { PersonContext } from '../context/personContext'
import Person from '../components/Person'
const Battlefield = () => {
const { person } = useContext(PersonContext)
const router = useRouter()
useEffect(() => {
if (!person.name) {
router.push('/')
}
})
return <Person />
}
export default Battlefield
My Test: src/tests/pages/Battlefield.spec.tsx
import { render, screen } from '#testing-library/react'
import { PersonContext } from '../../context/personContext'
import Battlefield from '../../pages'
jest.mock('../../components/Person', () => {
return {
__esModule: true,
default: function mock() {
return <div data-test-id="person" />
}
}
})
describe('Battlefield page', () => {
it('renders correctly', () => {
const mockPerson = { name: 'Darth Vader', avatar: 'darth-vader.png' }
const mockHandlePerson = jest.fn()
const mockLoading = false
render(
<PersonContext.Provider
value={{
person: mockPerson,
handlePerson: mockHandlePerson,
loading: mockLoading
}}
>
<Battlefield />
</PersonContext.Provider>
)
expect(screen.getByTestId('person')).toBeInTheDocument()
})
})
PRINSCREEN ERROR
enter image description here
I found a solution:
The error was happening because the path where I call the Battlefield page didn't have the absolute path.

How would one pass a component to a helper?

I want to pass a component to a helper and have that helper return an array of objects, each with a component node...
// helpers.ts
import { LINKS } from '../constants';
// error on the next line: Cannot find name 'Component'. ts(2304)
const createLinks = (component: Component) => {
return LINKS.map((props) => {
return ({
content: <Component {...props} />,
id: props.id
});
});
};
// component.tsx
import { List, SpecialLink } from '../components';
import { createLinks } from '../helpers';
const LinkList = () => {
const links = createLinks(SpecialLink);
return <List items={links}>
}
You should use the ComponentType type of react, so the component argument can be class component or function component.
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
import React from 'react';
import { ComponentType } from 'react';
const LINKS: any[] = [];
const createLinks = (Component: ComponentType) => {
return LINKS.map((props) => {
return {
content: <Component {...props} />,
id: props.id,
};
});
};

React create hooks with next.js

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

Resources