I'm managing a context for the classes of my school so that i don't have to fetch on every page, for this i write this custom component and wrap my entire app in it.I whenever i fetch the classes i store them in the redux store so that i can use it again and again.
And before fetch i check the store if the classes exist there i don't hit the API instead get the classes from there. in my dependency array i put navigate, dispatch and Classes because react was complaining about that. but this cause infinite loop
here is my code
// ** React Imports
import { createContext, useEffect, useState } from 'react';
import { useDispatch, connect } from 'react-redux';
// ** Config
import { toast } from 'react-hot-toast';
// ** custom imports
import { useNavigate } from 'react-router-dom';
import { CLASSES_API_HANDLER } from 'src/redux/actions/classes/classes';
// ** Defaults
const defaultProvider = {
classes: [],
setClasses: () => null,
loading: true,
setLoading: () => null,
};
export const ClassesContext = createContext(defaultProvider);
const ClassesProvider = ({ children, CLASSES }) => {
const dispatch = useDispatch();
const navigate=useNavigate()
const [classes, setClasses] = useState(defaultProvider.classes);
const [loading, setLoading] = useState(defaultProvider.loading);
useEffect(() => {
const getClasses = async () => {
if (CLASSES.length > 0) {
setClasses(CLASSES);
setLoading(false);
return;
}
try {
const Data = await dispatch(CLASSES_API_HANDLER());
setClasses(Data.result);
setLoading(false);
} catch (ex) {
navigate("/error")
console.log(ex);
}
}
getClasses()
}, [CLASSES,navigate,dispatch]);
const values = {
classes:classes,
setClasses:setClasses,
loading:loading,
setLoading:setLoading,
};
return (
<ClassesContext.Provider value={values}>
{children}
</ClassesContext.Provider>
);
};
function mapStateToProps(state) {
return { CLASSES: state.CLASSES };
}
export default connect(mapStateToProps)(ClassesProvider);
I'm trying to fix the infinite loop
Related
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;
I have created a new component in react
import React, {FC, useEffect} from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchExternalLinks } from '../../redux/reducers/appReducer'
import { getExternalLinksSelector } from '../../redux/selectors/appSelector'
type LinksType = {
title?: string | undefined
}
const ExternalLinks : FC<LinksType> = (props) => {
const {
title
} = props
const dispatch = useDispatch()
const links = useSelector(getExternalLinksSelector)
useEffect(() => {
console.log('use effect')
dispatch(fetchExternalLinks)
})
const openWindow = (path: string) => {
window.open(path, '_blank', 'toolbar=0,location=0,menubar=0');
}
return (
<>
Links:
{links.map((link) => {
return <a onClick={() => openWindow(link.path)}>{link.title}</a>
})
}
</>
)
}
export default ExternalLinks
when I try to call the fetchExternalLinks method from reducer with dispatch, it doesn't work.
In my console I can see only "use effect", but nothing from reducer
export const fetchExternalLinks = (): ThunkType => async (dispatch) => {
console.log("test")
try {
dispatch(appActions.toggleIsFetching(true))
const response = await appApi.getCustomers()
dispatch(appActions.setExternalLinks(response.data))
} catch (e) {
dispatch(appActions.toggleResponseMessage({isShown: true, isSuccess: false}))
} finally {
dispatch(appActions.toggleIsFetching(false))
}
}
Change your code from this,
dispatch(fetchExternalLinks)
to this,
dispatch(fetchExternalLinks())
Since you are only passing the reference, your async thunk is not getting executed, when the fetchExternalLinks function will be called using () rather than passing a reference you will receive an arrow function as a return value, to which react-redux will pass the dispatch function and will execute it for you
I currently have a component which builds its payload from the context stored in a redux store.
When completing the API call it then dispatches nextTask which updates the state of the context causing the effect to run again and be stuck in an infinite loop.
I need to be able to access the context when building the payload but only re-fire the fetch event when the url changes. I tried useCallback but may have misunderstood.
Here is the component:
import React, { useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import Axios from "../../config/AxiosConfig";
import { nextTask } from "../../redux/actions/flowActions";
import { RootState } from "../../redux/reducers/rootReducer";
import { JsonRPC } from "../../types/tasks";
const JsonRpc: React.FC<JsonRPC> = ({
url,
method,
payload,
payload_paths,
...rest
}) => {
const dispatch = useDispatch();
const context = useSelector((state: RootState) => state.context.present);
const getPayload = useCallback(() => {
console.log(context);
}, [context]);
useEffect(() => {
const fetch = async () => {
const payload = getPayload();
const { data } = await Axios({
method,
url,
// ...opts,
});
if (data.result) {
dispatch(nextTask());
}
};
try {
fetch();
} catch (e) {
console.log(e);
}
}, [url, dispatch, method, getPayload]);
return null;
};
export default JsonRpc;
I am trying to play background music in a React Native/Expo app as soon as the homeScreen loads.
Using expo AV library, I set up a MusicContext with a method to start the music. Here is my MusicContext.js
import React, {
useState,
useCallback,
useMemo,
useContext,
useEffect,
} from "react"
import PropTypes from "prop-types"
import { Audio } from "expo-av"
const initialState = {
playMusic: false,
setPlayMusic: () => null,
}
export const MusicContext = React.createContext(initialState)
const startMusic = async () => {
let mainTheme = null
if (!mainTheme) {
mainTheme = new Audio.Sound()
try {
console.log("trying")
await mainTheme.loadAsync(require("../assets/sounds/MainTheme.mp3"))
mainTheme.setStatusAsync({ isLooping: true })
} catch (error) {
console.log("Couldnt load main theme")
return
}
}
}
export const MusicProvider = props => {
const [playMusic, setPlayMusic] = useState(initialState.playMusic)
return (
<MusicContext.Provider
value={{
playMusic,
setPlayMusic,
startMusic,
}}
>
{props.children}
</MusicContext.Provider>
)
}
MusicContext.propTypes = {
children: PropTypes.node.isRequired,
}
I then call the the startMusic method in useEffect on my home screen.
const HomeScreen = () => {
const { startMusic } = useContext(MusicContext)
useEffect(() => {
console.log("running")
startMusic()
}, [])
I see all of the console.log output so I know it is running, but the music never plays. What am I missing here?
Is there a reason you aren't putting the startMusic in initialState?
When I put the function in the initalState, it gets called properly.
To answer your question more generally, you either need to wrap an aysnc function in an IIFE or have it as a separate function that is called.
I usually wrap as IIFE.
I have the following custom hook called useFlash:
import { useState } from 'react';
export default function useFlash() {
const [messages, setMessages] = useState([]);
const showFlash = (message: string) => {
setMessages([...messages, message]);
};
const clearMessage = (index: number) => {
setMessages(messages.filter((_m, i) => index !== i));
};
return {
messages,
showFlash,
clearMessage
};
}
Then I have this HOC providing it to two other components:
import React from 'react';
import useFlash from '../effects/useFlash';
const withFlash = (WrappedComponent: React.Component) => {
const WithFlash = () => {
const { messages, showFlash, clearMessage } = useFlash();
return (
<WrappedComponent
messages={messages}
showFlash={showFlash}
clearFlashMessage={clearMessage}
/>
);
};
return WithFlash;
};
export default withFlash;
It works well, except each use of the HOC gets its own state data. I need the state to be global. I know I can use contexts with consumer/providers, but I thought this way would be a little simpler. It is not proving to be true, is there a way to make this global?
You'll need to use Context, but it's not that bad..
create your context..
import React, { useState } from 'react';
export const FlashContext = React.createContext();
export const FlashProvider = ({ children }) => {
const [messages, setMessages] = useState([]);
return (
<FlashContext.Provider value={{ messages, setMessages }}>
{children}
</FlashContext.Provider>
);
};
wrap your components in the provider somewhere higher in the tree..
import React from "react";
import { FlashProvider } from "./flash-context";
const App = () => <FlashProvider><TheRest /></FlashProvider>;
export default App;
then use the context in your custom hook..
import React, { useContext } from "react";
import { FlashContext } from "./flash-context";
export default function useFlash() {
const { messages, setMessages } = useContext(FlashContext);
const showFlash = (message) => {
setMessages([...messages, message]);
};
const clearMessage = (index) => {
setMessages(messages.filter((_m, i) => index !== i));
};
return {
messages,
showFlash,
clearMessage
};
}