I'm working on my first serious NextJS app. I have it set up to pull in JSON data for the left nav, rather than hardcoding them in the app somewhere. This way I don't have to rebuild every time there's a minor change to the site's navigation.
Since the navigation needs to be available on every page, I added getInitialProps to the _app.js file, which grabs the left nav and passes it to the left nav component. But now as I'm moving on to build the homepage, I see that the getInitialProps there does not run. It seems that the getInitialProps in _app.js takes precendence.
Is there a way to have both? Or some other workaround that accomplishes the goal (or just a better way to do this in general)?
Note that I'm using getInitialProps for two reasons:
getStaticProps is out because I don't plan to build the entire site at build time
getServerSideProps is usually out because I don't like that it ends up doing two http requests: first a request goes to the NextJS server, then the server sends a request to my API (which happens to live on a different server). If I'm just getting basic stuff like the navigation, there's no need for getServerSideProps to run on the NextJS server, I'd rather skip the middle man
Here's some some simplified code:
_app.js:
import { Provider } from "react-redux";
import axios from "axios";
import store from "../store";
import Header from "../components/Header";
import LeftNav from "../components/LeftNav";
function MyApp(props) {
const { Component, pageProps } = props;
return (
<Provider store={store}>
<Header />
<LeftNav leftnav={props.leftnav} />
<Component { ...pageProps } />
</Provider>
)
}
MyApp.getInitialProps = async (context) => {
let config = await import("../config/config");
let response = await axios.get(`${config.default.apiEndpoint}&cAction=getLeftNav`);
if (response) {
return {
leftnav: response.data.leftNav
};
} else {
return {
leftnav: null
};
}
};
export default MyApp;
Home.js:
import axios from "axios";
const Home = (props) => {
console.log("Home props", props);
return (
<div>home</div>
);
};
Home.getInitialProps = async(context) => {
// this only runs if the getInitialProps in _app.js is removed :(
let config = await import("../config/config");
let response = await axios.get( `${config.default.apiEndpoint}&cAction=getHome` );
if ( response ) {
return {
home: response.data.home
};
} else {
return {
home: null
}
}
};
export default Home;
You have to call App.getInitialProps(context) in your _app to call the current page's getInitialProps. You can then merge the page's props with the remaining props from _app.
import App from 'next/app'
// Remaining code...
MyApp.getInitialProps = async (context) => {
const pageProps = await App.getInitialProps(context); // Retrieves page's `getInitialProps`
let config = await import("../config/config");
let response = await axios.get(`${config.default.apiEndpoint}&cAction=getLeftNav`);
return {
...pageProps,
leftnav: response?.data?.leftNav ?? null
};
};
From the custom _app documentation:
When you add getInitialProps in your custom app, you must import App from "next/app", call App.getInitialProps(appContext) inside
getInitialProps and merge the returned object into the return value.
Related
I'm using next-cookies to store auth credentials from user session, however, I can't get them during app initialization -let's say, the user refreshes the page or comes back later-, however, after app has been initialized -or user has loged in-, I get them navigating in the app.
This is important, because I want to fetch some initial data to be available in to the redux store from the beginning.
// pages/_app.js
import { useStore } from '../store/store';
import nextCookie from 'next-cookies';
function MyApp({ Component, pageProps }) {
const Layout = Component.layout || MainLayout;
const store = useStore(pageProps.initialReduxState); // custom useStore method to init store
const pageTitle = Component.title || pageProps.title || 'Título de la página';
return (
<Provider store={store}>
<Layout pageTitle={pageTitle}>
<Component {...pageProps} />
</Layout>
</Provider>
);
}
export default MyApp;
MyApp.getInitialProps = async (ctx) => {
const { token, user } = nextCookie(ctx);
/* TO DO
the idea is to get cookie from the server side
and pass it to the client side, if the cookie is
active, initial data will be triggered from the
initializers
*/
return { pageProps: { token, user } };
};
Check it out:
Is it a better way or native option to get the cookie without having to a different cookie dependency?
Btw, I already have a middleware that protects routes from non authenticated user which works fine:
// pages/_middleware.js
export function middleware(req) {
const activeSession = req.headers.get('cookie');
const url = req.nextUrl.clone();
if (activeSession) {
if (req.nextUrl.pathname === '/login') {
url.pathname = '/';
return NextResponse.redirect(url);
}
return NextResponse.next();
}
url.pathname = '/login';
return NextResponse.rewrite(url);
}
I have a straightforward react component that looks so in AllWords.js :
import React, { useEffect, useState } from 'react';
import consts from '../../constants/Constants.js';
function AllWords() {
const [words, setWords] = useState([]);
async function fetchData(){
const response= await fetch(consts.FETCH_URL);
const data = await (response.json());
setWords(data);
};
// API: useEffect( () => { . . . return cleanup; },[var_n_whose_change_triggers_useEffect . . .] );
useEffect(() => {fetchData()}, [] );
return (
<>
{
words.map(w=> <div>{w.word}</div>)
}
</>
);
}
export default AllWords;
I would like to refactor the fetchData() method out of the component into another file (basically a separate .js file that holds the fetch call).
What I would like is to have created a file titled FetchAllWords.js under src/actions/ & then import it. & use that.
I have several questions :
do I need to set the state in the FetchAllWords.js and then useSelector to extract the state in AllWords.js?
in FetchAllWords.js do I need to usedispatch to dispatch a method call setting the state? I would like to just setState in FetchAllWords.js and then extract it in AllWords.js. This is what I have so far:
import consts from '../constants/Constants.js';
import { useState } from 'react';
async function FetchAllWords(){
const [words, setWords] = useState([]);
const response= await fetch(consts.FETCH_URL);
const data = await (response.json());
setWords(data);
}
export default FetchAllWords;
I am unsure how to import this and use it in AllWords.js. I am using the following statement :
import wordList from '../../actions/FetchAllWords';
Then I am trying to use wordList as a handle to the file '../../actions/FetchAllWords.js' & attempting to access the async function FetchAllWords so wordList.FetchAllWords();
Firstly , the editor (VSCode) won't let me see the function despite the import call.
Secondly I am getting an error (something like) :
TypeError: _actions_FetchAllWords_js__WEBPACK_IMPORTED_MODULE_3__.default.FetchAllWords is not a function
Any insight or help would be appreciated since rather uneasy with JS & React.
The github repo is : https://github.com/mrarthurwhite/hooks-p5-react-redux
EDIT: As per David's suggestions :
So AllWords.js React component is :
import React, { useEffect, useState } from 'react';
import wordList from '../../services/Fetch.js';
function AllWords() {
const [words, setWords] = useState([]);
function fetchData(){
wordList.fetchAllWords().then(
data => setWords(data)
);
};
// API: useEffect( () => { . . . return cleanup; },[var_n_whose_change_triggers_useEffect . . .] );
useEffect(() => {fetchData()}, [] );
return (
<>
{
words.map(w=> <div>{w.word}</div>)
}
</>
);
}
export default AllWords;
And Fetch.js is :
import consts from '../constants/Constants.js';
class Fetch {
async fetchAllWords(){
const response= await fetch(consts.FETCH_URL);
const data = await (response.json());
return data;
}
}
export default Fetch;
No, don't worry about state in the external file. Just focus on the one thing it should do, perform the AJAX operation. At its simplest it's just a function, something like:
import consts from '../../constants/Constants.js';
const fetchAllWords = async () => {
const response = await fetch(consts.FETCH_URL);
const data = await (response.json());
return data;
}
export default fetchAllWords;
You can even make it a class which contains this function, if you plan on adding other service operations as well. (Fetch specific word? Find word? etc.) The point is that this does just one thing, provide data. Let the React components handle React state.
Within the component you'd just use that to get your data. Something like:
import React, { useEffect, useState } from 'react';
import fetchAllWords from '../../services/FetchAllWords.js';
function AllWords() {
const [words, setWords] = useState([]);
useEffect(() => {
fetchAllWords().then(w => setWords(w));
}, []);
return (
<>
{
words.map(w=> <div>{w.word}</div>)
}
</>
);
}
export default AllWords;
Overall it's a matter of separating concerns. The service performs the AJAX operation and returns the meaningful data, internally concerned with things like JSON deserialization and whatnot. The React component maintains the state and renders the output, internally concerned with updating state after useEffect runs and whatnot.
I have a function in a separate js file that checks status codes received from Api requests, and depending on the code this function needs to perform some actions:
function handleResponseCodes(res) {
try {
if (res.status ===200 ) {
return res.json();
} else if (res.status === 404) {
// here I need to redirect to /help
} else if (!res.ok) {
alert("Error")
} else {
if (res.ok) {
return res.data;
}
}
} catch (error) {
console.log(error);
}
}
and then this function is used like this with fetch requests.( project will have 100+ api requests so this way makes the process easy to follow).
fetch(url, obj)
.then((res) => handleResponseCodes(res)
If res.code === 404 I need to redirect the user to /help url, problem is that when I try to use useHistory() hook like this:
import {useHistory) from 'react-router-dom'
const history = useHistory()
//and in the function
else if (res.status === 404) {
// here I need to redirect to /help
history.push("/help")
I get error saying that useHistory hook must be used only in functional components. Is there a React way to redirect/push user to the /help from outside functional component?(basically from inside a function)
You can also use document.location.href = '/help'
You can just pass history as a parameter like this
...inside function component
const history = useHistory();
console.log(history)
//call function here
yourFunc(history)
Good question. We have so many api calls and have to handle those responses accordingly.
What I want to mention here is the close relation between routing and UI(for example, ProfilePage component).
According to Hook rules, we can't use hook function outside the React component, so we have to use the ways like #Zhang and #istar's answers.
But split the routing and component isn't a good practice. Of course some developers use routing config file for routing, but changing the url is usually being done in component.
I think routing is also one part of component.
So I want to recommend you that do the routing inside the component. Please take the result of response handling function and do the routing according to its result.
I had a same problem because of previous build functions.
import { useHistory } from 'react-router-dom';
function ReactComponent () {
const history = useHistory();
// some arrow function inside main react component
const someHandler = ( response, history ) => {
outsideFunction( response, history );
};
}
export default ReactComponent;
you can do as you like, but i am breaking it to two functions.
export function outsideFunction ( response, history ) {
if ( response.status === 201 ) {
checkedSubmitTypeHandler( location );
redirectPage(history);
return true;
}
}
export function redirectPage (history) {
return history.push(`/help`);
}
So, here is what happening that, you getting response wherever, Inside react main component or you handling in some other exported function. Passing the parameter through the arrow functions used inside react component.
It's not best practice to do it like this.
You should create a useHistoryHook which will be resuable in your application and you can call it like this.
// history.js page inside src
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
// useHistorypush component
import React from 'react';
import history from '../../history';
const useHistoryPush = (pathname,onLoadWait = 30) => {
const refContainer = React.useRef(false);
setTimeout(() => {
refContainer.current = true;
}, onLoadWait);
return (link) => {
if (refContainer.current) {
history.push(link);
}
};
};
export default useHistoryPush;
// implementation inside page
import useHistoryPush from '../use-history-push';
const Reactcomponent = () => {
const push = useHistoryPush();
React.useEffect(() => {
if (successResponse) {
// on some condition you can call it inside useeffect and it will redirect to the url mention inside *push
push(`/help`);
}
}, [successResponse, push]);
}
export default Reactcomponent;
I have a Next.js app, I'm using getInitialProps in my _app.js in order to be able to have persistent header and footer. However, I'm also needing to set data in a Context, and I need to be able to fetch the data based off of a cookie value. I've got the basics working just fine, however my _app sets the cookie on the first load, and then when I refresh the page it pulls in the appropriate data. I'm wondering if there's a way to be able to set the cookie first before fetching the data, ensuring that, if there's a cookie present, it will always pull in that data on the first load? Here is my _app.js, and, while I'm still working on the dynamic cookie value in my cookies.set method, I'm able to fetch the right data from my Prismic repo by hard-coding sacramento-ca for now, as you'll see. All I'm really needing is the logic to ensure that the cookie sets, and then the data fetches.
_app.js
import React from 'react';
import { AppLayout } from 'components/app-layout/AppLayout';
import { Footer } from 'components/footer/Footer';
import { Header } from 'components/header/Header';
import { LocationContext } from 'contexts/Contexts';
import Cookies from 'cookies';
import { Client } from 'lib/prismic';
import NextApp, { AppProps } from 'next/app';
import 'styles/base.scss';
import { AppProvider } from 'providers/app-provider/AppProvider';
interface WithNavProps extends AppProps {
navigation: any;
location: string;
dealer?: any;
cookie: string;
}
const App = ({ Component, pageProps, navigation, dealer }: WithNavProps) => {
const { Provider: LocationProvider } = LocationContext;
const locationData = dealer ? dealer : null;
return (
<LocationProvider value={{ locationData }}>
<AppProvider>
<AppLayout>
<Header navigation={navigation} location={dealer} />
<Component {...pageProps} />
<Footer navigation={navigation} />
</AppLayout>
</AppProvider>
</LocationProvider>
);
};
export default App;
App.getInitialProps = async (appContext: any) => {
const appProps = await NextApp.getInitialProps(appContext);
const cookies = new Cookies(appContext.ctx.req, appContext.ctx.res);
try {
cookies.set('dealerLocation', 'sacramento-ca', {
httpOnly: true,
});
const { data: navigation } = await Client.getByUID('navigation', 'main-navigation', {
lang: 'en-us',
});
const results = await Client.getByUID('dealer', cookies.get('dealerLocation'), {
lang: 'en-us',
});
return {
...appProps,
navigation,
dealer: results,
};
} catch {
const { data: navigation } = await Client.getByUID('navigation', 'main-navigation', {
lang: 'en-us',
});
return {
...appProps,
navigation,
};
}
};
I need to write a test with the following steps:
get user data on mount
get project details if it has selectedProject and clientId when they change
get pages details if it has selectedProject, clientId, and selectedPages when they change
render Content inside Switch
if doesn't have clientId, Content should return null
if doesn't have selectedProject, Content should return Projects
if doesn't have selectedPages, Content should return Pages
else Content should render Artboard
And the component looks like this:
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getUserData } from "../../firebase/user";
import { selectProject } from "../../actions/projects";
import { getItem } from "../../tools/localStorage";
import { getProjectDetails } from "../../firebase/projects";
import { selectPages } from "../../actions/pages";
import Pages from "../Pages";
import Projects from "../Projects";
import Artboard from "../Artboard";
import Switch from "../Transitions/Switch";
import { getUserId, getClientId } from "../../selectors/user";
import { getSelectedProject } from "../../selectors/projects";
import { getSelectedPages, getPagesWithDetails } from "../../selectors/pages";
import { getPagesDetails } from "../../firebase/pages";
const cachedProject = JSON.parse(getItem("selectedProject"));
const cachedPages = JSON.parse(getItem("selectedPages"));
const Dashboard = () => {
const dispatch = useDispatch();
const userId = useSelector(getUserId);
const clientId = useSelector(getClientId);
const selectedProject = useSelector(getSelectedProject) || cachedProject;
const selectedPages = useSelector(getSelectedPages) || cachedPages;
const pagesWithDetails = useSelector(getPagesWithDetails);
useEffect(() => {
dispatch(
getUserData(userId)
);
cachedProject && selectProject(cachedProject);
cachedPages && selectPages(cachedPages);
}, []);
useEffect(() => {
if (selectedProject && clientId) {
dispatch(
getProjectDetails(
clientId,
selectedProject
)
);
}
}, [selectedProject, clientId]);
useEffect(() => {
if (selectedPages && selectedProject && clientId) {
const pagesWithoutDetails = selectedPages.filter(pageId => (
!Object.keys(pagesWithDetails).includes(pageId)
));
dispatch(
getPagesDetails(
selectedProject,
pagesWithoutDetails
)
);
}
}, [selectedPages, selectedProject, clientId]);
const Content = () => {
if (!clientId) return null;
if (!selectedProject) {
return <Projects key="projects" />;
}
if (!selectedPages) {
return <Pages key="pages" />;
}
return <Artboard key="artboard" />;
};
console.log("Update Dashboard")
return (
<Switch>
{Content()}
</Switch>
);
};
Where I use some functions to fetch data from firebase, some to dispatch actions, and some conditionals.
I'm trying to get deep into testing with Jest and Enzyme. When I was searching for testing approaches, testing useEffect, variables, and conditions, I haven't found anything. All I saw is testing if a text changes, if a button has get clicked, etc. but what about testing components which aren't really changing anything in the DOM, just loading data, and depending on that data, renders a component?
What's the question here? What have you tried? To me it seems pretty straightforward to test:
Use Enzymes mount or shallow to render the component and assign that to a variable and wrap it in a store provider so it has access to a redux store.
Use jest.mock to mock things you don't want to actually want to happen (like the dispatching of actions) or use something like redux-mock-store.
Use that component ".find" to get the actual button you want.
Assert that, given a specific redux state, it renders correctly.
Assert that actions are dispatched with the proper type and payload at the proper times.
You may need to call component.update() to force it to rerender within the enzyme test.
Let me know if you have more specific issues.
Good luck!