React context api catalog lazy load - reactjs

In my react web app I need to load the whole catalog (metrics) and cache it on client side in order to let all components in my application use it. To store this catalog on client side I want to use react context api and I prefer to load this catalog lazily to avoid long app start.
Here's my solution:
I declare metrics context, exporting only one function getMetrics which returns promise from metrics array. This promise is created only once and is stored as Ref in context provider for future getMetrics executions.
metricsContext.tsx
import * as React from 'react'
import { createContext, useCallback, useRef } from 'react'
import { MetricModel, metricsApi } from '../api/metricsApi'
interface MetricsContext {
getMetrics: () => Promise<MetricModel[]>
}
export const metricsContext = createContext<MetricsContext>({ getMetrics: () => Promise.resolve([]) })
interface Props {
children: React.ReactNode
}
export const MetricsContextProvider = ({ children }: Props) => {
const metricsPromise = useRef<Promise<MetricModel[]>>()
const getMetrics = useCallback(async () => {
if (metricsPromise.current == null) {
metricsPromise.current = metricsApi.getAll()
}
return await metricsPromise.current
}, [])
const { Provider } = metricsContext
return <Provider value={{ getMetrics: getMetrics }}>{children}</Provider>
}
To use this context in the component I need to import context into it and resolve promise by executing useEffect.
SomeComponent.tsx
import * as React from 'react'
import { useContext, useEffect, useState } from 'react'
import { MetricModel } from '../../../api/metricsApi'
import { metricsContext } from '../../../state/metricsContext'
export const SomeComponent = () => {
const cntxt = useContext(metricsContext)
const [metrics, setMetrics] = useState<MetricModel[]>([])
useEffect(() => {
cntxt.getMetrics().then(res => setMetrics(res))
}, [cntxt])
return metrics.map(m => <div key={m.id}>{m.name}</div>)
}
So my questions are:
Is it normal to store Promise in ref?
Is it normal that context provider makes http requests lazily?

Related

Jest Mock returns undefined instead of value

I am using Jest to test a react component. I am trying to mock a function from other dependency. The function from dependency should return an array, but it is showing undefined on the console.
Below file is the tsx file, when I click the button, it should call the dependency function to get the list of the Frames.
ExitAppButton.tsx:
import React, { useContext, useState } from 'react';
import { TestContext } from '../ContextProvider';
import { useDispatch } from 'react-redux';
const ExitAppButton = (props: any): JSX.Element => {
const { sdkInstance } = useContext(TestContext);
const exitAppClicked = () => {
const appList = sdkInstance.getFrames().filter((app: any) => {app.appType === "Test App"}).length}
test file, SignOutOverlay.test.tsx:
import * as React from 'react';
import { fireEvent, render, screen } from '#testing-library/react';
import SignOutOverlay from '.';
import ExitAppButton from './ExitAppButton';
import { TestContext } from '../ContextProvider';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
const api = require('#praestosf/container-sdk/src/api');
const mockStore = configureStore([]);
jest.mock('#praestosf/container-sdk/src/api');
api.getFrames.mockReturnValue([{appType:"Test App"},{appType:"Test App"},{appType:"Not Test App"}]);
describe('Test Exit app Button', () => {
const renderExitAppButton = () => {
const store = mockStore([{}]);
render(
<Provider store={store}>
<TestContext.Provider value={{ sdkInstance: api }}>
<SignOutOverlay>
<ExitAppButton/>
</SignOutOverlay>
</TestContext.Provider>
</Provider>
);
};
it('should to be clicked and logged out', () => {
renderExitAppButton();
fireEvent.click(screen.getByTestId('exit-app-button-id'));
});
This is the dependency file, api.js
const getFrames = () => {
let frames = window.sessionStorage.getItem('TestList');
frames = frames ? JSON.parse(frames) : [];
return frames
};
const API = function () { };
API.prototype = {
constructor: API,
getFrames
};
module.exports = new API();
I mocked the getFrame function to return an array of 3 objects, but when running the test case, it is returning undefined. Below error was showing:
TypeError: Cannot read property 'filter' of undefined
Am I mocking this correct?
I think it's because api.getFrames is undefined and not a mock.
Try changing your mock statement to this:
jest.mock('#praestosf/container-sdk/src/api', () => ({
getFrames: jest.fn(),
// add more functions if needed
}));
Turns out, I have the other file with the same test name which is causing the problem. I am beginner for Jest, a tip for developer like me, we should always run test case file alone using
jest file.test.tsx
Not all files at a time:
jest

How to create HOC to wrap useContext provider in React?

I want to reuse a context provider in different parts of my app using HOC ("higher order components"), but my state does not get updated.
This is the wrapper of the provider.
import React, { FC, useState } from "react";
import AdminContext from "./adminContext";
const AdminContextWrapper: FC = ({ children }) => {
const [authAdmin, setAuthAdmin] = useState<boolean | null>(null);
const value = { authAdmin, setAuthAdmin };
return (
<AdminContext.Provider value={value}>{children}</AdminContext.Provider>
);
};
export default AdminContextWrapper;
This is how I am implementing it :
import { useContext } from "react";
import AdminContext from "#comp/contexts/adminContext";
import AdminLogin from "#comp/admin/adminLogin";
import Limit from "#comp/admin/limits";
import AdminContextWrapper from "#comp/contexts/adminWrapper";
const Admin = () => {
const { authAdmin } = useContext(AdminContext);
const AdminPage = () => {
return (
<div>
<Limit />
</div>
);
};
return (
<AdminContextWrapper>
{authAdmin ? <AdminPage /> : <AdminLogin />}
</AdminContextWrapper>
);
Finally, this is my context:
import { createContext } from "react";
import { AdminContextType } from "#comp/utils/types";
const InitialUserContext: AdminContextType = {
authAdmin: false,
setAuthAdmin: (authAdmin: boolean | null) => {},
};
const AdminContext = createContext<AdminContextType>(InitialUserContext);
export default AdminContext;
I can see the state change in the login page but the admin page is not getting the update.
adminLogin.tsx
//...
const { setAuthAdmin, authAdmin } = useContext(AdminContext);
useEffect(() => {
console.log(authAdmin); // returns true after validating but the admin does not update.
}, [authAdmin]);
//...
I highly appreciate any help. Thank you.
Unless I'm misreading things, in
const Admin = () => {
const { authAdmin } = useContext(AdminContext);
// ...
return (
<AdminContextWrapper>
{authAdmin ? <AdminPage /> : <AdminLogin />}
</AdminContextWrapper>
);
}
you're trying to use the context outside its provider AdminContextWrapper - useContext would return undefined there unless you're already nested within another provider for the admin context, in which case the inner AdminContextWrapper there would give the inner components a different admin context.
You may want to make sure there's only ever exactly one admin context.
(As an aside, the // ... above used to be a nested component in your original code. Never do that – nested components' identity changes on each update, causing spurious re-renders.)

React hooks useState getting diferrent value from redux state

I have react component look like this following code:
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useParams } from "react-router-dom";
import { createClient, getClients } from "../redux/actions/clients";
function UpdateClient(props) {
let params = useParams();
const { error, successSubmit, clients } = useSelector(
(state) => state.clients
);
const [client, setClient] = useState(clients[0]);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getClients({ id: params.id }));
}, []);
const submitClient = () => {
dispatch(createClient(client));
};
return (
<div>{client.name} {clients[0].name}</div>
);
}
export default UpdateClient;
And the result is different client.name return test1,
while clients[0].name return correct data based on route parameter id (in this example parameter id value is 7) which is test7
I need the local state for temporary saving form data. I don't know .. why it's become different?
Can you please help me guys? Thanks in advance
You are referencing a stale state which is a copy of the clients state.
If you want to see an updated state you should use useEffect for that.
useEffect(() => {
setClient(clients[0]);
}, [clients]);
Notice that duplicating state is not recommended.
There should be a single “source of truth” for any data that changes in a React application.

How to write a test for conditional rendering component depended on useState hook in React?

I'm trying to write a test for my functional component, but don't understand how to mock isRoomsLoaded to be true, so I could properly test my UI. How and what do I need to mock?
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchRooms } from '../../store/roomsStore'; // Action creator
// Rooms component
export default ({ match, location, history }) => {
const roomsStore = useSelector(state => state.rooms);
const dispatch = useDispatch();
const [isRoomsLoaded, setRoomsLoaded] = useState(false);
useEffect(() => {
const asyncDispatch = async () => {
await dispatch(fetchRooms());
setRoomsLoaded(true); // When data have been fetched -> render UI
};
asyncDispatch();
}, [dispatch]);
return isRoomsLoaded
? <RoomsList /> // Abstraction for UI that I want to test
: <LoadingSpinner />;
};
If you want, you could flat out mock useState to just return true or false, to get whichever result you want by doing the following.
const mockSetState = jest.fn();
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: value => [true, mockSetState],
}));
By doing this, you're effective mocking react, with react, except useState, its a bit hacky but it'll work.

Can I replace context with hooks?

Is there a way with new react hooks API to replace a context data fetch?
If you need to load user profile and use it almost everywhere, first you create context and export it:
export const ProfileContext = React.createContext()
Then you import in top component, load data and use provider, like this:
import { ProfileContext } from 'src/shared/ProfileContext'
<ProfileContext.Provider
value={{ profile: profile, reloadProfile: reloadProfile }}
>
<Site />
</ProfileContext.Provider>
Then in some other components you import profile data like this:
import { ProfileContext } from 'src/shared/ProfileContext'
const context = useContext(profile);
But there is a way to export some function with hooks that will have state and share profile with any component that want to get data?
React provides a useContext hook to make use of Context, which has a signature like
const context = useContext(Context);
useContext accepts a context object (the value returned from
React.createContext) and returns the current context value, as given
by the nearest context provider for the given context.
When the provider updates, this Hook will trigger a rerender with the
latest context value.
You can make use of it in your component like
import { ProfileContext } from 'src/shared/ProfileContext'
const Site = () => {
const context = useContext(ProfileContext);
// make use of context values here
}
However if you want to make use of the same context in every component and don't want to import the ProfileContext everywhere you could simply write a custom hook like
import { ProfileContext } from 'src/shared/ProfileContext'
const useProfileContext = () => {
const context = useContext(ProfileContext);
return context;
}
and use it in the components like
const Site = () => {
const context = useProfileContext();
}
However as far a creating a hook which shares data among different component is concerned, Hooks have an instance of the data for them self and don'tshare it unless you make use of Context;
updated:
My previous answer was - You can use custom-hooks with useState for that purpose, but it was wrong because of this fact:
Do two components using the same Hook share state? No. Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
The right answer how to do it with useContext() provided #ShubhamKhatri
Now i use it like this.
Contexts.js - all context export from one place
export { ClickEventContextProvider,ClickEventContext} from '../contexts/ClickEventContext'
export { PopupContextProvider, PopupContext } from '../contexts/PopupContext'
export { ThemeContextProvider, ThemeContext } from '../contexts/ThemeContext'
export { ProfileContextProvider, ProfileContext } from '../contexts/ProfileContext'
export { WindowSizeContextProvider, WindowSizeContext } from '../contexts/WindowSizeContext'
ClickEventContext.js - one of context examples:
import React, { useState, useEffect } from 'react'
export const ClickEventContext = React.createContext(null)
export const ClickEventContextProvider = props => {
const [clickEvent, clickEventSet] = useState(false)
const handleClick = e => clickEventSet(e)
useEffect(() => {
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('click', handleClick)
}
}, [])
return (
<ClickEventContext.Provider value={{ clickEvent }}>
{props.children}
</ClickEventContext.Provider>
)
}
import and use:
import React, { useContext, useEffect } from 'react'
import { ClickEventContext } from 'shared/Contexts'
export function Modal({ show, children }) {
const { clickEvent } = useContext(ClickEventContext)
useEffect(() => {
console.log(clickEvent.target)
}, [clickEvent])
return <DivModal show={show}>{children}</DivModal>
}

Resources