I am using PrimeReact's toast component, whose API looks like this:
function App() {
const toast = useRef(null);
useEffect(() => {
toast.current.show({
severity: 'info',
detail: 'Hellope'
});
});
return (
<div className='App'>
<Toast ref={toast} />
</div>
);
}
I would now like to call toast.current.show() from a non-React context. In particular, I have an http() utility function through which all HTTP calls are made. Whenever one fails, I would like to show a toast. What are clean/idiomatic ways to achieve this?
Initialize the toast on the window object.
useLayoutEffect(() => {
window.PrimeToast = toast.current || {};
}, []);
On your fetch or axios handler, use the above object on your error handler
const fakeUrl = "https://api.afakeurl.com/hello";
fetch(fakeUrl)
.then((res) => res.data)
.catch((err) => {
console.error("error fetching request", err);
if (window.PrimeToast) {
window.PrimeToast.show({
severity: "error",
summary: "Error calling https",
detail: "hello"
});
}
});
Updated Sandbox
Reference:
https://www.primefaces.org/primereact/toast/
I would create a toast context that would allow showing toasts
toast-context.js
import "primereact/resources/themes/lara-light-indigo/theme.css";
import "primereact/resources/primereact.css";
import { Toast } from "primereact/toast";
import { createContext, useContext, useRef } from "react";
// create context
const ToastContext = createContext(undefined);
// wrap context provider to add functionality
export const ToastContextProvider = ({ children }) => {
const toastRef = useRef(null);
const showToast = (options) => {
if (!toastRef.current) return;
toastRef.current.show(options);
};
return (
<ToastContext.Provider value={{ showToast }}>
<Toast ref={toastRef} />
<div>{children}</div>
</ToastContext.Provider>
);
};
export const useToastContext = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error(
"useToastContext have to be used within ToastContextProvider"
);
}
return context;
};
index.js
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { ToastContextProvider } from "./toast-context";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<ToastContextProvider>
<App />
</ToastContextProvider>
</StrictMode>
);
App.js
import { useToastContext } from "./toast-context";
export default function App() {
// use context to get the showToast function
const { showToast } = useToastContext();
const handleClick = () => {
http(showToast);
};
return (
<div className="App">
<button onClick={handleClick}>show toast</button>
</div>
);
}
// pass showToast callback to your http function
function http(showToast) {
showToast({
severity: "success",
summary: "Success Message",
detail: "Order submitted"
});
}
Codesanbox example: https://codesandbox.io/s/beautiful-cray-rzrfne?file=/src/App.js
Here is one solution I have been experimenting with, although I have the impression it isn't very idiomatic. I suppose one could look at it as a "micro-frontend" responsible exclusively for showing toasts.
import ReactDOM from 'react-dom/client';
import { RefObject, useRef } from 'react';
import { Toast, ToastMessage } from 'primereact/toast';
class NotificationService {
private toast?: RefObject<Toast>;
constructor() {
const toastAppRoot = document.createElement('div');
document.body.append(toastAppRoot);
const ToastApp = () => {
this.toast = useRef<Toast>(null);
return <Toast ref={this.toast} />;
};
ReactDOM.createRoot(toastAppRoot).render(<ToastApp />);
}
showToast(message: ToastMessage) {
this.toast!.current!.show(message);
}
}
export const notificationService = new NotificationService();
The simplicity of its usage is what's really nice of an approach like this. Import the service, call its method. It used to be that simple.
Related
I have Const like this in my config.service.ts file
export const mysettings={
userid:"12324",
conf:{
sessionDuration:30,
mac:"LON124"
}
}
I am using this constant in some components
But instead of hardcoding those values in const I need to get that at runtime from JSON file in my public folder
So I have a function like this as well
async getConfig(){
const data=await fetch("./data/data.json")
.then((response) => response.json())
.then((json) => return json );
}
So in my data.json files I have those values for the const and I need these values in that JSON file to updated or the JSON file itself sometimes replaced.
Please help me how to accomplish that?
You can create a Context and ContextProvider to load your data from json file, set it to state variable and just pass it to Context consumers:
DataContext.tsx:
import { createContext, useState, useContext, useEffect, PropsWithChildren } from "react";
import asyncData from "./asyncData";
interface IData {
userid: string;
conf: {
sessionDuration: number;
mac: string;
};
}
interface IContextValue {
data: IData;
}
const StateContext = createContext<IContextValue>(null!);
export function DataContextProvider(props: PropsWithChildren) {
const [data, setData] = useState<IData>(undefined!);
useEffect(() => {
asyncData.then((json) => setData(json)).catch(console.error);
}, []);
// Optionally - wrap with useMemo
const contextValue: IContextValue = {
data: data
};
return (
<StateContext.Provider value={contextValue}>
{data && props.children}
</StateContext.Provider>
);
}
export default function useDataContext() {
const context = useContext(StateContext);
if (!context) {
throw new Error(
"useDataContext must be used within the DataContextProvider"
);
}
return context;
}
Updates to index.tsx:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { DataContextProvider } from "./DataContext";
import App from "./App";
import asyncData from "./asyncData";
asyncData.then((config) => {
console.log("Config is fetched before render: ", config);
const rootElement = document.getElementById("root");
const root = createRoot(rootElement!);
root.render(
<StrictMode>
<DataContextProvider>
<App />
</DataContextProvider>
</StrictMode>
);
});
Usage:
import useDataContext from "./DataContext";
export default function App() {
const { data } = useDataContext();
return <div className="App">{JSON.stringify(data)}</div>;
}
asyncData.ts:
const asyncData = fetch("./data/data.json").then((response) => response.json());
export default asyncData;
I have the following app entry component:
React.useEffect(() => {
const fetchData = async () => {
try {
const libraries: unknown[] = await sendRequest('/libraries');
const softwareComponents: unknown[] = await sendRequest('/softwareComponents');
localStorage.setItem('libraries', JSON.stringify(arraySetup(libraries, 'libraries')));
localStorage.setItem('softwareComponents', JSON.stringify(arraySetup(softwareComponents, 'software-components')));
} catch (err) {
console.error(err);
}
};
isAuthenticated() && fetchData();
}, []);
I am fetching Arrays from two endpoints and then set the result in the Local Storage, so I can read from it in other components.
A child component is using the data like this:
const [data, setData] = React.useState<Array<any>>([]);
React.useEffect(() => {
const libraries = getLocalStorageItem('libraries');
const softwareComponents = getLocalStorageItem('softwareComponents');
const condition = libraries && softwareComponents;
if (condition) {
setData([...libraries, ...softwareComponents]);
}
}, []);
const getDataLength = (category: string) => {
return (data || []).filter((item: any) => item.category === category).length;
};
return (
<React.Fragment>
<OwcGrid item xs={12} s={4}>
<LibrariesCard numberOfElements={getDataLength('libraries')} /> // rendering here the length of the localStorage item.
</OwcGrid>
Goal/Challenge:
I want to use React.Context to remove local storage implementation but I am not sure how to keep it as simple as possible.
I only saw guides which implemented dispatch actions and so on but this seems already too complex because I only fetch the data and don't change it as I only render it.
Are there any tipps or guides how to start with this?
Possible implementation with context:
//context.tsx
import {
createContext,
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
export interface LibsAndComponentsInterface {
data: unknown[];
}
const LibsAndComponentsContext = createContext<
LibsAndComponentsInterface | undefined
>(undefined);
// Wrap your App component with this
export function LibsAndComponentsProvider({
children,
}: {
children: ReactNode;
}) {
const [libs, setLibs] = useState<unknown[]>([]);
const [components, setComponents] = useState<unknown[]>([]);
useEffect(() => {
const fetchData = async () => {
try {
const libraries: unknown[] = await sendRequest('/libraries');
const softwareComponents: unknown[] = await sendRequest(
'/softwareComponents'
);
setLibs(libraries);
setComponents(softwareComponents);
} catch (err) {
console.error(err);
}
};
isAuthenticated() && fetchData();
}, []);
const ctxValue = useMemo(
() => ({
data: [...libs, ...components],
}),
[libs, components]
);
return (
<LibsAndComponentsContext.Provider value={ctxValue}>
{children}
</LibsAndComponentsContext.Provider>
);
}
export function useLibsAndComponents() {
const ctx = useContext(LibsAndComponentsContext);
if (ctx == null) {
throw new Error(
'useLibsAndComponents must be inside LibsAndComponentsProvider'
);
}
return ctx;
}
// later in your components
const { data } = useLibsAndComponents()
Here is the complete setup for React Context. Please use typescript if needed.
MyContextProvider.js
const { createContext, useState } = require("react");
//Create a context
export const Mycontext = createContext();
//Created a component that helps to provide the context.
const MyContextProvider = ({ children }) => {
//Declare all the states that you need
const [libraries, setLibraries] = useState([]);
const [softwareComponents, setSoftwareComponents] = useState([]);
return (
<Mycontext.Provider
//provide all the state, function as value that you need in any child component
value={{
libraries,
setLibraries,
softwareComponents,
setSoftwareComponents
}}
>
{children}
</Mycontext.Provider>
);
};
export default MyContextProvider;
index.js
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import MyContextProvider from "./MyContextProvider";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
//Wrap App component By the MycontextProvider component
root.render(
<StrictMode>
<MyContextProvider>
<App />
</MyContextProvider>
</StrictMode>
);
App.js
import { useContext } from "react";
import ChildComponent from "./Child";
import { Mycontext } from "./MyContextProvider";
import "./styles.css";
export default function App() {
//This is the way of getting value from context here useContext is the builtin hook and Mycontext is the context name
const { setLibraries, setSoftwareComponents } = useContext(Mycontext);
//After getting data from API use setLibraries and setSoftwareComponents to store data in the state(context) instead of local storage.
return (
<div>
<ChildComponent />
</div>
);
}
Child.js
import { useContext } from "react";
import { Mycontext } from "./MyContextProvider";
const ChildComponent = () => {
const { libraries, softwareComponents } = useContext(Mycontext);
//Here is your state you can use it as your need.
console.log(libraries, softwareComponents);
return <h1>Child Component</h1>;
};
export default ChildComponent;
The code works but console log shows it renders twice. I want to reduce unnecessary rerenders/API fetching. Solutions tried include async-await, try-catch, useMemo, React.Memo, StrictMode in index.js (just quadruples the console log entries).
I prepared a codesandbox.
This is the console log:
Console logs twice every time
In the sandbox I use a fake API compared to my actual project code which uses Axios. The outcome is the same.
SearchContext (Context Provider) fetches from API once, then BlogList (Context Consumer) fetches again and console logs the second time.
React Version: 18.2.0
SearchContext.js
import React, { createContext, useState, useEffect, useMemo, useCallback } from 'react';
import axios from 'axios';
const SearchContext = createContext();
export const SearchContextProvider = ({ children }) => {
const [blog, setBlog] = useState([]);
const value = useMemo(() => ([ blog, setBlog ]), [blog]);
// const value = React.memo(() => ([ blog, setBlog ]), [blog]);
// I try 'useMemo, React.memo' (above):
// I try 'try-catch':
// const retrieveBlog = () => {
// try {
// const response = axios.get(`${process.env.REACT_APP_API_URL}/api/v2/pages/?type=blog.Blog&fields=image,description`)
// .then(response => {
// setBlog(response.data.items);
// console.log('thisiscontextprovider(blog):', blog)
// })
// }
// catch (err) {
// console.log(err);
// }
// }
// useEffect(() => {
// retrieveBlog();
// }, []);
// I try 'useCallback, if, async-await, try-catch' :
const retrieveBlog = useCallback(async () => {
if (blog.length === 0){
try {
const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v2/pages/?type=blog.Blog&fields=image,description`)
.then(response => {
setBlog(response.data.items);
console.log('thisiscontextprovider(blog):', blog)
})
}
catch (err) {
console.log(err);
}
}
})
useEffect(() => {
retrieveBlog()
}, []);
// I try 'async-await':
// useEffect(() => {
// async function retrieveBlog() {
// try {
// const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v2/pages/?type=blog.Blog&fields=image,description`)
// .then(response => {
// setBlog(response.data.items);
// console.log('thisiscontextprovider(blog):', blog)
// })
// }
// catch (err) {
// console.log(err);
// }
// }
// retrieveBlog()
// }, []);
return (
<div>
<SearchContext.Provider value={value}>
{/* <SearchContext.Provider value={[blog, setBlog]}> */}
{children}
{console.log('from context provider return:', blog)}
</SearchContext.Provider>
</div>
);
};
export default SearchContext;
BlogList.js
import React, { useContext, useCallback, useEffect } from 'react';
import styled from 'styled-components';
import SearchContext from "../contexts/SearchContext";
import { SearchContextProvider } from "../contexts/SearchContext";
const BlogCard = styled.div`
`;
const BlogList = () => {
const [blog, setBlog] = useContext(SearchContext);
return (
<div>
<SearchContextProvider>
BlogList
<BlogCard>
{blog.map((blog) => (
<li key={blog.id}>
{blog.title}
</li>
))}
</BlogCard>
</SearchContextProvider>
</div>
);
};
export default BlogList;
App.js
import React from "react";
import { Route, Routes } from "react-router-dom";
import styled from "styled-components";
import BlogList from "./components/BlogList";
import { SearchContextProvider } from "./contexts/SearchContext";
const Wrapper = styled.h1``;
function App() {
return (
<Wrapper>
<p>
This CodeSandbox is for my question on StackOverflow. Console logs
twice.
</p>
<SearchContextProvider>
<Routes>
<Route exact path="" element={<BlogList />} />
{/* <Route exact path="/blog" element={<BlogList />} /> */}
</Routes>
</SearchContextProvider>
</Wrapper>
);
}
export default App;
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from "react-router-dom";
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
reportWebVitals();
I am keen to understand why this happens, rather than just solving it.
I'm trying to use the useSnack hook from notistack library but I keep getting this error
TypeError: Cannot destructure property 'enqueueSnackbar' of 'Object(...)(...)' as it is undefined.
Here is the code:
import React, { useContext, useEffect } from "react";
import AlertContext from "../context/alert/alertContext";
import { SnackbarProvider, useSnackbar } from "notistack";
const Alerts = (props) => {
const alertContext = useContext(AlertContext);
// This line below is where the error seems to be
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
alertContext.msg !== "" &&
enqueueSnackbar(alertContext.msg, {
variant: alertContext.type,
});
}, [alertContext]);
return <SnackbarProvider maxSnack={4}>{props.children}</SnackbarProvider>;
};
export default Alerts;
useSnackbar hook accessible anywhere down the tree from SnackbarProvider.
So you cannot use it in the same component as SnackbarProvier.
import AlertContext from "../context/alert/alertContext";
import { SnackbarProvider } from "notistack";
const Alerts = (props) => {
const alertContext = useContext(AlertContext);
const providerRef = React.useRef();
useEffect(() => {
alertContext.msg !== "" &&
providerRef.current.enqueueSnackbar(alertContext.msg, {
variant: alertContext.type,
});
}, [alertContext]);
return <SnackbarProvider ref={providerRef} maxSnack={4}>
{props.children}
</SnackbarProvider>;
};
export default Alerts;
Wrap you index file with SnapBar provider:
index.js
import { SnackbarProvider } from "notistack";
const Index = () => (
<SnackbarProvider maxSnack={1} preventDuplicate>
index
</SnackbarProvider>
)
export default Index
jsx file
import { useSnackbar } from "notistack";
const Logs = () => {
const { enqueueSnackbar } = useSnackbar();
const handler = () => {
enqueueSnackbar(`Successful.`, { variant: "success" });
};
return <span onClick={handler}>"Logs loading"</span>;
};
export default Logs;
I want add screen loading in next js project. And I tried to do that with the Router component in next/router.
This is my _app.js in next.js project:
import {CookiesProvider} from 'react-cookie';
import App from 'next/app'
import React from 'react'
import {Provider} from 'react-redux'
import withRedux from 'next-redux-wrapper'
import withReduxSaga from 'next-redux-saga'
import createStore from '../src/redux/store'
import Router from "next/router";
import {Loaded, Loading} from "../src/util/Utils";
class MyApp extends App {
static async getInitialProps({Component, ctx}) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps({ctx})
}
return {pageProps}
}
render() {
Router.onRouteChangeStart = () => {
Loading()
};
Router.onRouteChangeComplete = () => {
Loaded()
};
Router.onRouteChangeError = () => {
Loaded()
};
const {Component, pageProps, store} = this.props;
return (
<CookiesProvider>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</CookiesProvider>
)
}
}
export default withRedux(createStore)(withReduxSaga(MyApp))
This is Loaded() and Loading() functions:
export const Loaded = () => {
setTimeout(() => {
let loading = 'has-loading';
document.body.classList.remove(loading);
}, 100);
};
export const Loading = () => {
let loading = 'has-loading';
document.body.classList.add(loading);
};
The code works well when the project is under development mode. But when the project is built, the loading won't disappear.
Do you know solution of this issue or are you suggesting another solution?
Using apollo client and react hooks you could do as follow.
Example:
import { useQuery } from '#apollo/react-hooks';
import gql from 'graphql-tag';
import { withApollo } from '../lib/apollo';
import UserCard from '../components/UserCard';
export const USER_INFO_QUERY = gql`
query getUser ($login: String!) {
user(login: $login) {
name
bio
avatarUrl
url
}
}
`;
const Index = () => {
const { query } = useRouter();
const { login = 'default' } = query;
const { loading, error, data } = useQuery(USER_INFO_QUERY, {
variables: { login },
});
if (loading) return 'Loading...'; // Loading component
if (error) return `Error! ${error.message}`; // Error component
const { user } = data;
return (
<UserCard
float
href={user.url}
headerImg="example.jpg"
avatarImg={user.avatarUrl}
name={user.name}
bio={user.bio}
/>
);
};
export default withApollo({ ssr: true })(Index);
More info here: https://github.com/zeit/next.js/tree/canary/examples/with-apollo
I added the following codes to a wrapper component and the problem was resolved.
componentDidMount() {
Loaded();
}
componentWillUnmount() {
Loading();
}