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.
Related
I am trying to use context with my Gatsby project. I have successfully implemented this in my previous project and I have copied the code over to my new project and it's not working as intended.
This is my context.js file:
import React, { useContext, useState } from "react";
const defaultState = {
isLoggedIn: false,
};
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [isLoggedIn, setIsLoggedIn] = useState(false);
function toggle() {
console.log("BOO!");
}
const value = {
isLoggedIn,
setIsLoggedIn,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
This is my app.js file:
import React from "react";
import { Router } from "#reach/router";
import IndexPage from "./index";
import ProjectPage from "./project";
import { AuthProvider } from "../contexts/context";
const App = () => (
<AuthProvider>
<Router basepath="/app">
<IndexPage path="/" component={IndexPage} />
<ProjectPage path="/project" component={ProjectPage} />
</Router>
</AuthProvider>
);
export default App;
This is my index.js file:
import React, { useContext } from "react";
import { Link } from "gatsby";
import { useAuth } from "../contexts/context";
import { AuthContext } from "../contexts/context";
const IndexPage = () => {
console.log(useAuth())
return (
<div className="w-40 h-40 bg-red-400">
{/*<Link to="/project">to projects</Link>*/}
<div>Click me to toggle: uh</div>
</div>
);
};
export default IndexPage;
useAuth() should return the desired components and functions but instead is always returning undefined. I have looked over my previous code as well as snippets on stack overflow and I can't seem to find the correct fix.
The following includes code that successfully built and executed:
Original context.js
import '#stripe/stripe-js'
/* Functionality */
import React, { useContext, useEffect, useState } from "react";
import { navigate } from "#reach/router";
import firebase from 'gatsby-plugin-firebase';
import { useLocalStorage } from 'react-use';
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [isLoading, setIsLoading] = useLocalStorage("loading", false);
// Sign In
const signInWithRedirect = (source) => {
let provider;
switch(source) {
case 'Google':
provider = new firebase.auth.GoogleAuthProvider()
break;
case 'Github':
provider = new firebase.auth.GithubAuthProvider()
break;
default:
break;
}
setIsLoading(true)
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION)
.then(() => {
// Existing and future Auth states are now persisted in the current
// session only. Closing the window would clear any existing state even
// If a user forgets to sign out.
// ...
// New sign-in will be persisted with session persistence.
return firebase.auth().signInWithRedirect(provider)
})
.catch((error) => {
// Handle Errors here.
let errorCode = error.code;
let errorMessage = error.message;
});
}
// Sign Out
const signOut = () => {
firebase.auth().signOut().then(() => {
// Sign-out successful.
setIsLoggedIn(false)
navigate('/app/login')
}).catch((error) => {
// An error happened.
});
}
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
try {
// If user is authenticated
if (!!user) {
// Fetch firestore document reference
var docRef = firebase.firestore().collection("study_guide_customers").doc(user.uid)
docRef.get().then((doc) => {
console.log('checking doc')
// If the document doesn't exist, create it and add to the firestore database
if (!doc.exists) {
console.log('inside customer')
const customer = {
customerCreationTimestamp: firebase.firestore.Timestamp.now(),
username: user.displayName,
email: user.email
}
firebase.firestore().collection("study_guide_customers").doc(user.uid).set(customer)
.then(() => {
// After docuement for user is created, set login status
setIsLoggedIn(!!user)
setIsLoading(false)
})
.catch((error) => {
console.error("Error writing document: ", error);
});
// If document for user exists, set login status
} else {
setIsLoggedIn(!!user)
setIsLoading(false)
}
})
}
} catch {
console.log('Error checking firestore existence and logging in...')
}
})
}, [isLoggedIn, isLoading, setIsLoading, setIsLoggedIn])
const value = {
signOut,
isLoggedIn,
isLoading,
setIsLoading,
setIsLoggedIn,
signInWithRedirect,
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
Original app.js
/* Stripe Security */
import '#stripe/stripe-js'
/* Functionality */
import React from "react"
import { Router } from "#reach/router"
import PrivateRoute from "../components/PrivateRoute"
import Profile from "../components/Profile"
import Login from "../components/Login"
import Projects from "../components/Projects"
import IndexPage from "./index"
import NotFoundPage from './404'
import { AuthProvider } from "../contexts/context"
const App = () => (
<AuthProvider>
<Router basepath="/app">
<PrivateRoute path="/profile" component={Profile} />
<Login path="/login" component={Login}/>
<IndexPage path="/" component={IndexPage}/>
<Projects path="/projects" component={Projects} />
</Router>
</AuthProvider>
)
export default App
Original index.js
/* Stripe Security */
import '#stripe/stripe-js'
/* Functionality */
import * as React from "react"
import IndexContact from "../components/Index/Contact"
import IndexSelectedProjects from "../components/Index/SelectedProjects"
import IndexFeaturedProjects from "../components/Index/FeaturedProjects"
import IndexFooter from "../components/Index/Footer"
import IndexStudyGuide from "../components/Index/StudyGuide"
import IndexNavbar from "../components/Index/Navbar"
import IndexHeader from "../components/Index/Header"
import IndexAbout from '../components/Index/About'
import IndexExperience from '../components/Index/Experience'
import { useMount } from 'react-use';
const IndexPage = () => {
useMount(() => localStorage.setItem('loading', false));
return (
<>
<IndexNavbar />
<IndexHeader />
<IndexAbout />
<IndexExperience />
<IndexFeaturedProjects />
<IndexSelectedProjects />
<IndexStudyGuide />
<IndexContact />
<IndexFooter />
</>
)
}
export default IndexPage
Then in any component I could simply use the following code to access the context
import { useAuth } from "../contexts/context"
const { isLoggedIn, signInWithRedirect, isLoading } = useAuth()
Child components are mounted before parent. Fix your context.js file to add a default value for isLoggedIn state:
const defaultState = {
isLoggedIn: false,
setIsLoggedIn: () => {}
};
const AuthContext = React.createContext(defaultState);
Your defaultState should also include default methods for any parts of the context you wish to work with.
I am using context that updates my snackbar content from a single place. So whenever I want to display snackbar in some other component as a consumer, I update its value. Everything is functionally working but my tests are failing.
I am using mui snackbar.
For the reference: I am using react testing library coupled with jest.
Note: What I have done already:
waitFor with timeout property
Wrapped ContextProvider in render function
This is my code:
HOME.JS
import React from 'react';
import {Suspense} from 'react';
import {useState} from 'react';
import {Route} from 'react-router-dom';
import {Switch} from 'react-router-dom';
import {useLocation} from 'react-router-dom';
import withStyles from '#mui/styles/withStyles';
import BillingDetails from 'views/BillingDetails/BillingDetails';
import ErrorBoundary from 'containers/ErrorBoundary/ErrorBoundary';
import PageNotFound from 'views/PageNotFound/PageNotFound';
import SideBar from 'components/SideBar/SideBar';
import Snackbar from 'components/CustomSnackbar/CustomSnackbar';
import Unauthorized from 'views/Unauthorized/Unauthorized';
import {subRoutesConfig} from 'constants/routeConfig';
import {styles} from './styles';
import {BILLING_OVERDUE} from 'views/BillingDetails/constants';
import SidebarJobsContext from 'context/sidebarJobsContext';
import SnackbarContext from 'context/snackbarContext';
import {PropTypes} from 'prop-types';
const useStyles = (theme) => ({
...styles(theme),
});
const Home = (props) => {
let location = useLocation();
const [sidebarJobs, setSidebarJobs] = useState([]);
const [updateJobs, setUpdateJobs] = useState(false);
const [snackbar, setSnackbar] = useState({});
const sidebarJobsContextValue = {sidebarJobs, setSidebarJobs, updateJobs, setUpdateJobs};
const snackbarContextValue = {snackbar, setSnackbar};
const handleSnackbarClose = (reason, onClose) => {
if (reason == 'clickaway') {
return;
}
onClose && onClose();
setSnackbar({...snackbar, open: false});
};
const showSnackbar = () => {
const {
autoHideDuration,
text,
severity,
onExternalLink,
open=false,
onClose,
handleUndo,
} = snackbar;
return <Snackbar
autoHideDuration={autoHideDuration}
handleClose={(event, reason) => handleSnackbarClose(reason, onClose)}
handleExternalLink={onExternalLink}
handleUndo={handleUndo}
open={open}
severity={severity}
text={text}
/>;
};
const {
classes,
} = props;
const currentURLPath = location.pathname;
return (
<>
<SidebarJobsContext.Provider value={sidebarJobsContextValue}>
<SnackbarContext.Provider value={snackbarContextValue}>
<SideBar/>
<div className={classes.root}>
<div className={classes.offset}>
<ErrorBoundary key={currentURLPath}>
<Suspense fallback={<span></span>}>
<Switch>
{
(redirect || billingInfo.subscription_expiry) && <Route>
<BillingDetails/>
</Route>
}
{subRoutesConfig.map((route, i) => {
let component = isRouteAllowed(route) ? route.component : Unauthorized;
return (
<Route
component={component}
exact={route.exact}
key={route}
path={route.path}
/>
);
})}
<Route component={PageNotFound} />
</Switch>
{showProductTour()}
{showSnackbar()}
</Suspense>
</ErrorBoundary>
</div>
</div>
</SnackbarContext.Provider>
</SidebarJobsContext.Provider>
</>
);
};
Home.propTypes = {
classes: PropTypes.object,
};
export default withStyles(useStyles)(Home);
As you can see this is the Home.js component where I have added the provider and showSnackbar() takes care of displaying snackbar.
and then I am using useContext in functional and contextType in class components.
There are many tests that are failing here's an example of one:
import React from 'react';
import {fireEvent} from '#testing-library/react';
import {render} from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import {waitFor} from '#testing-library/react';
import {convertIntoRGB} from 'utilities/utils';
import {ThemeWrapper} from 'utilities/testUtilities';
import {error} from 'constants/colors';
import {success} from 'constants/colors';
import CompanySettings from '../CompanySettings';
import SnackbarContext from '../../app/src/context/snackbarContext';
let snackbarContextValue = {
snackbar: {
text: '',
severity: 'success',
open: false,
autoHideDuration: 3000,
handleUndo: () => {},
onClose: () => {},
},
setSnackbar: () => {},
};
const component = (props) => {
return (render(<ThemeWrapper
storeIncluded={true}>
<SnackbarContext.Provider value={snackbarContextValue}>
<CompanySettings />
</SnackbarContext.Provider>
</ThemeWrapper>));
};
describe('CompanySettings', () => {
test('should Render success snackbar', async () => {
const {getByText, getByTestId, container, queryAllByTestId} = component();
expect(getByText('Company logo')).toBeInTheDocument();
expect(getByTestId('custom-icon-Package')).toBeVisible();
expect(queryAllByTestId('custom-skeleton').length).toBe(8);
await waitFor(()=> {
expect(queryAllByTestId('custom-skeleton').length).toBe(4);
let editBtn = getByText(/Edit company profile/i);
fireEvent.click(editBtn);
const inputField = container.querySelector(`input[name=companyName]`);
userEvent.type(inputField, ' 1');
fireEvent.click(getByTestId('saveBtn'));
});
await waitFor(() => {
expect(getByText('Gomez Inc 1')).toBeInTheDocument();
expect(container.querySelector(`input[name=companyName]`)).not.toBeInTheDocument();
});
await waitFor(() => {
expect(getByText(`Changes saved to 'company profile'.`)).toBeInTheDocument();
expect(getComputedStyle(getByTestId('alert')).backgroundColor).toEqual(convertIntoRGB(success));
});
});
});
The last waitFor in this test is failing as I have used context to display this snackbar content that says Changes saved to 'company profile'.
I am trying to use [Redux] to update the state in my app. I can't get the dispatch method to work in fetchUser. Spare me, I'm a bit of a noob.
All I want to do is have fetchUser run when [useEffect]runs on the first render.
The error I'm getting is
dispatch is not a function. (In 'dispatch({type:actions_types_WEBPACK_IMPORTED_MODULE_12_["FETCH_USER"], payload: result })', 'dispatch' is undefined)
Its the last useEffect in the main js file.
This is the index js file:
import 'react-app-polyfill/ie9';
import 'react-app-polyfill/stable';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import reduxThunk from 'redux-thunk';
import App from './App';
import Main from './Main';
import './helpers/initFA';
import reducers from './reducers';
const store = createStore(reducers, {}, applyMiddleware(reduxThunk));
ReactDOM.render(
<Provider store={store}>
<Main>
<App />
</Main>
</Provider>
,
document.getElementById('main')
);
Here is the Main js file:
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import AppContext from './context/Context';
import { settings } from './config';
import toggleStylesheet from './helpers/toggleStylesheet';
import { getItemFromStore, setItemToStore, themeColors } from './helpers/utils';
import { connect } from 'react-redux';
import * as actions from './actions';
import axios from 'axios';
import { FETCH_USER } from './actions/types';
const Main = props => {
const [isFluid, setIsFluid] = useState(getItemFromStore('isFluid', settings.isFluid));
const [isRTL, setIsRTL] = useState(getItemFromStore('isRTL', settings.isRTL));
const [isDark, setIsDark] = useState(getItemFromStore('isDark', settings.isDark));
const [isTopNav, setIsTopNav] = useState(getItemFromStore('isTopNav', settings.isTopNav));
const [isCombo, setIsCombo] = useState(getItemFromStore('isCombo', settings.isCombo));
const [isVertical, setIsVertical] = useState(getItemFromStore('isVertical', settings.isVertical));
const [isNavbarVerticalCollapsed, setIsNavbarVerticalCollapsed] = useState(
getItemFromStore('isNavbarVerticalCollapsed', settings.isNavbarVerticalCollapsed)
);
const [currency, setCurrency] = useState(settings.currency);
const [showBurgerMenu, setShowBurgerMenu] = useState(settings.showBurgerMenu);
const [isLoaded, setIsLoaded] = useState(false);
const [isOpenSidePanel, setIsOpenSidePanel] = useState(false);
const [navbarCollapsed, setNavbarCollapsed] = useState(false);
const [navbarStyle, setNavbarStyle] = useState(getItemFromStore('navbarStyle', settings.navbarStyle));
const toggleModal = () => setIsOpenSidePanel(prevIsOpenSidePanel => !prevIsOpenSidePanel);
const value = {
isRTL,
isDark,
isCombo,
isFluid,
setIsRTL,
isTopNav,
currency,
setIsDark,
setIsCombo,
setIsFluid,
isVertical,
toggleModal,
setIsTopNav,
navbarStyle,
setCurrency,
setIsVertical,
showBurgerMenu,
setNavbarStyle,
isOpenSidePanel,
navbarCollapsed,
setShowBurgerMenu,
setIsOpenSidePanel,
setNavbarCollapsed,
isNavbarVerticalCollapsed,
setIsNavbarVerticalCollapsed
};
const setStylesheetMode = mode => {
setIsLoaded(false);
setItemToStore(mode, value[mode]);
toggleStylesheet({ isRTL, isDark }, () => setIsLoaded(true));
};
useEffect(() => {
setStylesheetMode('isFluid');
// eslint-disable-next-line
}, [isFluid]);
useEffect(() => {
setStylesheetMode('isRTL');
// eslint-disable-next-line
}, [isRTL]);
useEffect(() => {
setStylesheetMode('isDark');
// eslint-disable-next-line
}, [isDark]);
useEffect(() => {
setItemToStore('isNavbarVerticalCollapsed', isNavbarVerticalCollapsed);
// eslint-disable-next-line
}, [isNavbarVerticalCollapsed]);
useEffect(() => {
setItemToStore('isTopNav', isTopNav);
// eslint-disable-next-line
}, [isTopNav]);
useEffect(() => {
setItemToStore('isCombo', isCombo);
// eslint-disable-next-line
}, [isCombo]);
useEffect(() => {
setItemToStore('isVertical', isVertical);
// eslint-disable-next-line
}, [isVertical]);
useEffect(() => {
setItemToStore('navbarStyle', navbarStyle);
// eslint-disable-next-line
}, [navbarStyle]);
useEffect((dispatch) => {
const result = axios.get('/api/current_user')
dispatch({type: FETCH_USER, payload: result });
}, []);
if (!isLoaded) {
toggleStylesheet({ isRTL, isDark }, () => setIsLoaded(true));
return (
<div
style={{
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0,
backgroundColor: isDark ? themeColors.dark : themeColors.light
}}
/>
);
}
return <AppContext.Provider value={value}>{props.children}</AppContext.Provider>;
};
Main.propTypes = { children: PropTypes.node };
export default connect(null, actions)(Main);
Here is the App js file:
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import Layout from './layouts/Layout';
import 'react-toastify/dist/ReactToastify.min.css';
import 'react-datetime/css/react-datetime.css';
import 'react-image-lightbox/style.css';
const App = () => {
return (
<Router basename={process.env.PUBLIC_URL}>
<Layout />
</Router>
);
};
export default App;
Here is the client/src/layouts/Layout js file:
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { toast, ToastContainer } from 'react-toastify';
import { CloseButton, Fade } from '../components/common/Toast';
import DashboardLayout from './DashboardLayout';
import ErrorLayout from './ErrorLayout';
import loadable from '#loadable/component';
const AuthBasicLayout = loadable(() => import('./AuthBasicLayout'));
const Landing = loadable(() => import('../components/landing/Landing'));
const WizardLayout = loadable(() => import('../components/auth/wizard/WizardLayout'));
const AuthCardRoutes = loadable(() => import('../components/auth/card/AuthCardRoutes'));
const AuthSplitRoutes = loadable(() => import('../components/auth/split/AuthSplitRoutes'));
const Layout = () => {
useEffect(() => {
AuthBasicLayout.preload();
Landing.preload();
WizardLayout.preload();
AuthCardRoutes.preload();
AuthSplitRoutes.preload();
}, []);
return (
<Router fallback={<span />}>
<Switch>
<Route path="/" exact component={Landing} />
<Route path="/authentication/card" component={AuthCardRoutes} />
<Route path="/authentication/split" component={AuthSplitRoutes} />
<Route path="/authentication/wizard" component={WizardLayout} />
<Route path="/errors" component={ErrorLayout} />
<Route path="/authentication/basic" component={AuthBasicLayout} />
<Route path="/dashboard" component={DashboardLayout} />
</Switch>
<ToastContainer transition={Fade} closeButton={<CloseButton />} position=. {toast.POSITION.BOTTOM_LEFT} />
</Router>
);
};
export default Layout;
import { useDispatch } from 'react-redux';
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(someFetchActionCreator());
}, [])
}
For this example, I would recommend using useDispatch() from the react-redux function library. The above answer does not consider the async, and the result will simply return a Promise to the dispatch payload.
I am developing a React JS, Redux, GraphQL, TypeScript app.
And I would like to know how to invoke the function that fetches data and updates the state via GraphQL from my container.
The name of the action that loads the data via GraphQL is appActions.getAppData();
But it causes an infinite refresh loop because it triggers (StatusActions.startAppLoading()); which updates the state as well.
I would like to know how to fix this issue or how to rewrite /Main/index.tsx as a class component and invoke startAppLoading() from componentDidMount().
Thank you in advance.
main.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createBrowserHistory } from 'history';
import { configureStore } from 'app/store';
import { Router } from 'react-router';
import { App } from './app';
// prepare store
const history = createBrowserHistory();
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<App />
</Router>
</Provider>,
document.getElementById('root')
);
app/index.tsx
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { App as Main } from 'app/containers/Main';
import { hot } from 'react-hot-loader';
let currentContainer = Main;
export const App = hot(module)(() => (
<Switch>
<Route exact path="/" component={currentContainer} />
<Route path="*">
<Redirect to="https://google.com" />
</Route>
</Switch>
));
app/containers/Main/index.tsx
import React from 'react';
import style from './style.css';
import { RouteComponentProps } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import { useTodoActions } from 'app/actions';
import { useAppActions } from 'app/actions';
import { RootState } from 'app/reducers';
import { Header, TodoList, Footer } from 'app/components';
export namespace App {
export interface Props extends RouteComponentProps<void> {}
}
export const App = ({ history, location }: App.Props) => {
const dispatch = useDispatch();
const appActions = useAppActions(dispatch);
const { apps } = useSelector((state: RootState) => {
return {
apps: state.apps
};
});
appActions.getAppData();
return (
<div className={style.normal}>
<Header />
<TodoList appActions={appActions} apps={apps} />
<Footer />
</div>
);
};
app/actions/apps.ts
export const getAppData = () => {
let appKey = 'interpegasus';
return (dispatch: Dispatch) => {
dispatch(StatusActions.startAppLoading());
debugger;
apolloClient
.query({
query: gql`
query getApp($appKey: String!) {
getApp(id: $appKey) {
id
name
domain
}
}
`,
variables: {
appKey: appKey
}
})
.then((result) => {
debugger;
if (result.data.apps.length > 0) {
dispatch(populateAppData(result.data.apps[0]));
}
dispatch(StatusActions.endAppLoading());
})
.catch((error) => {
dispatch(StatusActions.endAppLoading());
console.log({
error: error
});
});
};
};
You should put your appActions.getAppData() inside useEffect hooks like this
useEffect(()=>{
appActions.getAppData()
},[])
check the official docs Introducing Hooks
In Main/index.tsx, you are calling appActions.getAppData(); which will lead you to actions/apps.ts. Here, you are doing dispatch(StatusActions.startAppLoading()); which will update the state and re-render ``Main/index.tsx`. Then again you call getAppData() and the loop continues to lead to infinite loop.
Call the api only if not loading.
Something like this:
...
const { apps, loading } = useSelector((state: RootState) => {
return {
apps: state.apps,
loading: state.loading // <----- replace with your actual name of your state
};
});
if(!loading){
appActions.getAppData();
}
...
I'm trying to control the page using react-hook, react-router-dom, redux.
The login screen is implemented and the code that tries to switch to the main page when the login is successful is written.
So I used history.push of react-router.
However, history.push only changes the browser url, not the actual information displayed on the page.
The code is shown below.
In brief code explanation,
The id and password are put into redux and get as getelementbyuId and sent as form data.
The login function works normally.
history.push ('/') written to "userUpdateUserInfo" doesn't work at all.
Only the url of the browser changes, not the main page.
App.tsx
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { Main, Login } from './pages';
import './css/App.css';
const App: React.FC = () => {
return (
<div>
<div className="App-contents-area">
<Switch>
<Route exact path="/" component={Login} />
<Route exact path="/main" component={Main} />
{/* <Redirect path="*" to="/" /> */}
</Switch>
</div>
</div>
);
}
export default App;
LoginPage.tsx
import React from 'react';
import { Login } from 'Component';
function LoginPage() {
return (
<Login />
);
}
export default LoginPage;
Login.tsx (components)
import React from 'react';
import {
LoginTitle, LoginAvatar, LoginUserId, LoginUserPassword, LoginButton
} from '..';
import '../../css/Login.css';
function Login() {
return (
<div className="Login">
<div className="Login-form-data">
<LoginTitle /> // code skip
<LoginAvatar /> // code skip
<LoginUserId /> // code skip
<LoginUserPassword /> // code skip
<LoginButton />
</div>
</div>
);
}
export default Login;
LoginButton.tsx (components)
import React from 'react';
import { useUpdateUserInfo } from 'Hook';
function LoginButton() {
const { handleLogin } = useUpdateUserInfo(); // custom hook
return (
<div className="LoginButton">
<button className="LoginButton-button" onClick={handleLogin}>Login</button>
</div>
);
}
export default LoginButton;
userUpdateUserInfo.tsx (custom hook)
import { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from 'Store/modules';
import { updateUserInfo } from 'Store/modules/user';
import { userLoginStatus } from 'Store/modules/login';
import { msg } from 'Lang';
import {
axiosPost, history,
_ShowFail, _ShowSuccess, _ShowSelect
} from 'Module';
export default function useUpdateUserInfo () {
const { id, name, tel, email } = useSelector((state: RootState) => state.user);
let { isLogin } = useSelector((state: RootState) => state.login);
const dispatch = useDispatch();
const handleLogin = useCallback(async () => {
try {
const userId: string = (document.getElementById('LoginUserId-id') as HTMLInputElement).value.trim();
const userPw: string = (document.getElementById('LoginUserPassword-password') as HTMLInputElement).value.trim();
if (!userId.length) { return _ShowFail(msg.pleaseInputUserId); }
if (!userPw.length) { return _ShowFail(msg.pleaseInputUserPassword); }
const formData: FormData = new FormData();
formData.append('userId', userId);
formData.append('userPw', userPw);
const url = '/login/check-login-info';
const config = {
headers: {
'Content-Type': 'multipart/form-data',
},
};
const res = await axiosPost(url, formData, config);
if (res.data.res) {
_ShowSuccess('login success');
const userInfo = {
id: res.data.rows[0].id,
name: res.data.rows[0].name,
email: res.data.rows[0].email,
tel: res.data.rows[0].tel,
};
isLogin = true;
/**************************/
history.push('/main'); // now working
/**************************/
dispatch(updateUserInfo(userInfo));
dispatch(userLoginStatus({ isLogin }));
}
else {
_ShowFail('login fail');
isLogin = false;
dispatch(updateUserInfo({ id, email, name, tel }));
dispatch(userLoginStatus({ isLogin }));
}
}
catch (error) {
_ShowFail(error.message);
}
}, [dispatch]);
return { handleLogin };
};
MainPage.tsx
import React from 'react';
function MainPage() {
return (
<div>
<h2>MainPage!!</h2>
</div>
);
}
export default MainPage;
history.tsx
import { createBrowserHistory } from 'history'
export default createBrowserHistory();
Since last night, I have been suffering from this problem.
How can I change the content of the page?
In App.js your Routes suppose to be inside < Router > < /Router >,
https://reacttraining.com/react-router/web/api/Router
(unless you wrap it on index.js that not including here and App is imported inside there)?