Skip to content

front-end-intermediate/7-nextjs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NextJS

Homework

Incorporate images into the project.

Exercise

Scaffold a NextJS project:

npx create-next-app@latest next-project

Note: running start in a NextJS app runs from the production build folder. Use npm run dev instead.

Things to note:

  1. CSS modules
  2. Pages folder
  3. _app file

PokeAPI

https://pokemondb.net/pokedex/national

Add images, pokemon.json and logo.svg to the public folder.

Document

There is no HTML document in our NextJS project. We cannot add a favicon.

Add _document.js:

import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html>
      <Head>
        <link rel="icon" href="logo.svg" type="image/svg+xml" />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

Load and Display Pokemon

In a new pages/pokemon.js file:

import React from "react";

const PokemonRow = ({ pokemon }) => (
  <tr key={pokemon.id}>
    <td>{pokemon.name.english}</td>
    <td>{pokemon.type.join(", ")}</td>
  </tr>
);

export default function Pokemon() {
  const [pokemon, pokemonSet] = React.useState(null);

  React.useEffect(() => {
    fetch("/pokemon.json")
      .then((resp) => resp.json())
      .then((data) => pokemonSet(data));
  }, []);

  if (!pokemon) {
    return <div>Loading data...</div>;
  }

  return (
    <div>
      <h1>Pokemon Search</h1>
      <div>
        <table width="100%">
          <tbody>
            {pokemon.map((pokemon) => (
              <PokemonRow key={pokemon.id} pokemon={pokemon} />
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

Display Pokemon Details

Still working in pages/pokemon.js, add a new PokemonInfo component and a button to the Rows.

const PokemonRow = ({ pokemon, onClick }) => (
  <tr key={pokemon.id}>
    <td>{pokemon.name.english}</td>
    <td>{pokemon.type.join(", ")}</td>
    {/* NEW table cell */}
    <td>
      <button onClick={() => onClick(pokemon)}>More Information</button>
    </td>
  </tr>
);

// NEW component
const PokemonInfo = ({ name: { english }, base }) => (
  <div>
    <h2>{english}</h2>
    <table>
      <tbody>
        {Object.keys(base).map((key) => (
          <tr key={key}>
            <td>{key}</td>
            <td>{base[key]}</td>
          </tr>
        ))}
      </tbody>
    </table>
  </div>
);

Note the use of Object.keys() - returns an array of strings of the property names.

An example:

const base = {
  HP: 45,
  Attack: 49,
  Defense: 49,
  "Sp. Attack": 65,
  "Sp. Defense": 65,
  Speed: 45,
};

let objKeys = Object.keys(base).map((key) => {
  return `${key} : ${base[key]}`;
});

console.log(objKeys);

Compare Object.values() and Object.entries().

Add selectedPokemon state to the Pokemon component and compose the new PokemonInfo component.

export default function Pokemon() {
  const [pokemon, pokemonSet] = React.useState(null);
  // NEW
  const [selectedPokemon, selectedPokemonSet] = React.useState(null);

  React.useEffect(() => {
    fetch("/pokemon.json")
      .then((resp) => resp.json())
      .then((data) => pokemonSet(data));
  }, []);

  if (!pokemon) {
    return <div>Loading data...</div>;
  }

  return (
    <div>
      <h1>Pokemon Search</h1>
      <div>
        <div>
          <table width="100%">
            <tbody>
              {pokemon.map((pokemon) => (
                <PokemonRow
                  key={pokemon.id}
                  pokemon={pokemon}
                  onClick={(pokemon) => selectedPokemonSet(pokemon)}
                />
              ))}
            </tbody>
          </table>
        </div>
        {selectedPokemon && <PokemonInfo {...selectedPokemon} />}
      </div>
    </div>
  );
}

Test the button.

That's a lot of pokemon. We will restrict the number and then allow the user to filter them.

Restrict

Array.slice() returns an array:

<tbody>
  {pokemon.slice(0, 20).map((pokemon) => (
    <PokemonRow
      key={pokemon.id}
      pokemon={pokemon}
      onClick={(pokemon) => selectedPokemonSet(pokemon)}
    />
  ))}
</tbody>

Filter Pokemon

We will add a new filter state and setter, an input field, and then use the filtered data as the source of displayed pokemon.

const [filter, filterSet] = React.useState("");
...
<input
type="text"
value={filter}
onChange={(event) => filterSet(event.target.value)}
/>
...
.filter((pokemon) =>
pokemon.name.english
  .toLowerCase()
  .includes(filter.toLowerCase())
)

Here's the end result:

export default function Pokemon() {
  // NEW
  const [filter, filterSet] = React.useState("");
  const [pokemon, pokemonSet] = React.useState(null);
  const [selectedPokemon, selectedPokemonSet] = React.useState(null);

  React.useEffect(() => {
    fetch("/pokemon.json")
      .then((resp) => resp.json())
      .then((data) => pokemonSet(data));
  }, []);

  if (!pokemon) {
    return <div>Loading data</div>;
  }

  return (
    <div>
      <h1>Pokemon Search</h1>
      <div>
        <div>
          <input
            type="text"
            value={filter}
            onChange={(event) => filterSet(event.target.value)}
          />
          <table width="100%">
            <tbody>
              {pokemon
                .filter(({ name: { english } }) =>
                  english
                    .toLocaleLowerCase()
                    .includes(filter.toLocaleLowerCase())
                )
                .slice(0, 20)
                .map((pokemon) => (
                  <PokemonRow
                    key={pokemon.id}
                    pokemon={pokemon}
                    onClick={(pokemon) => selectedPokemonSet(pokemon)}
                  />
                ))}
            </tbody>
          </table>
        </div>
        {selectedPokemon && <PokemonInfo {...selectedPokemon} />}
      </div>
    </div>
  );
}

Note the use of toLocaleLowerCase to convert the English name to lower case.

Material UI

We will use Material UI as a source of ready made components.

Install Material UI:

npm install @mui/material @emotion/react @emotion/styled

Try using an MUI Button component in PokemonRow:

import React from "react";
// NEW
import { Button } from "@mui/material";

const PokemonRow = ({ pokemon, onClick }) => (
  <>
    <tr key={pokemon.id}>
      <td>{pokemon.name.english}</td>
      <td>{pokemon.type.join(", ")}</td>
      <td>
        {/* NEW */}
        <Button
          variant="contained"
          color="primary"
          onClick={() => onClick(pokemon)}
        >
          More Information
        </Button>
      </td>
    </tr>
  </>
);

Use an MUI Text Field.

import { Button, TextField } from "@mui/material";
...
 <TextField
   variant="standard"
   type="search"
   label="Filter Pokemon"
   value={filter}
   onChange={(event) => filterSet(event.target.value)}
 />

At this point, depending on your light/dark system appearance preferences, they may be display issues.

Try importing the theme provider and setting the mode to either dark or light:

import { ThemeProvider, createTheme } from "@mui/material/styles";

const darkTheme = createTheme({
  palette: {
    mode: "dark",
  },
});

...

  return (
    <ThemeProvider theme={darkTheme}>
    ...
    </ThemeProvider>

Styled Components

MUI uses Emotion internally. Emotion is similar to Styled Components and can be used without MUI.

MUI offers page layout tools but we will use Emotion instead.

import styled from "@emotion/styled";

// ...

const Title = styled.h1`
  text-align: center;
`;
const PageContainer = styled.div`
  margin: auto;
  width: 800px;
  padding-top: 1em;
`;
const TwoColumnLayout = styled.div`
  display: grid;
  grid-template-columns: 80% 20%;
  grid-column-gap: 1rem;
`;
const StyledTextField = styled(TextField)`
  width: 100%;
  padding: 0.2rem;
  font-size: large;
  margin: 2rem 0;
`;

export default function Pokemon() {
  const [filter, filterSet] = React.useState("");
  const [pokemon, pokemonSet] = React.useState(null);
  const [selectedPokemon, selectedPokemonSet] = React.useState(null);

  React.useEffect(() => {
    fetch("/pokemon.json")
      .then((resp) => resp.json())
      .then((data) => pokemonSet(data));
  }, []);

  if (!pokemon) {
    return <div>Loading data</div>;
  }

  return (
    <PageContainer>
      <Title>Pokemon Search</Title>
      <TwoColumnLayout>
        <div>
          <StyledTextField
            variant="standard"
            type="search"
            value={filter}
            label="Filter Pokemon"
            onChange={(event) => filterSet(event.target.value)}
          />
          <table width="100%">
            <tbody>
              {pokemon
                .filter(({ name: { english } }) =>
                  english
                    .toLocaleLowerCase()
                    .includes(filter.toLocaleLowerCase())
                )
                .slice(0, 20)
                .map((pokemon) => (
                  <PokemonRow
                    key={pokemon.id}
                    pokemon={pokemon}
                    onClick={(pokemon) => selectedPokemonSet(pokemon)}
                  />
                ))}
            </tbody>
          </table>
        </div>
        {selectedPokemon && <PokemonInfo {...selectedPokemon} />}
      </TwoColumnLayout>
    </PageContainer>
  );
}

Note the syntax for the StyledTextField component.

Navigation

Create a Nav component components/nav.js:

import Link from "next/link";

export default function Nav() {
  return (
    <nav>
      <li>
        <Link href="/">Home</Link>
      </li>
      <li>
        <Link href="/pokemon">Pokemon</Link>
      </li>
      <li>
        <Link href="/movies">Movies</Link>
      </li>
    </nav>
  );
}

Import and compose it in _document.js. Note the page refresh. This is obviously incorrect. We need to create a layout:

// components/layout.js

import Nav from "./nav";

export default function Layout({ children }) {
  return (
    <>
      <Nav />
      <main>{children}</main>
    </>
  );
}

And compose it in _app.js:

import "../styles/globals.css";
import type { AppProps } from "next/app";
import Layout from "../components/layout";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

Update Nav to use MUI following this formula:

import Link from "next/link";
import styled from "@emotion/styled";

import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";

const StyledLink = styled(Link)`
  color: white;
  text-decoration: none;
  padding: 0.5rem;
`;

export default function Nav() {
  return (
    <Box sx={{ flexGrow: 1 }}>
      <AppBar position="static">
        <Toolbar>
          <IconButton
            size="large"
            edge="start"
            color="inherit"
            aria-label="menu"
            sx={{ mr: 2 }}
          >
            <MenuIcon />
          </IconButton>
          <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
            <StyledLink href="/">Home</StyledLink>
            <StyledLink href="/pokemon">Pokemon</StyledLink>
            <StyledLink href="/movies">Movies</StyledLink>
          </Typography>
          <Button color="inherit">Login</Button>
        </Toolbar>
      </AppBar>
    </Box>
  );
}

Note: we'll need it install @mui/icons-material for the above.

NextJS Dynamic Routes

NextJS Dynamic Routes are routing utilities for creating pages that use parameters.

We'll use them to display one page per pokemon.

Create components/PokemonRow.js and move the component from pokemon.js:

import { Button } from "@mui/material";

export const PokemonRow = ({ pokemon, onClick }) => (
  <tr key={pokemon.id}>
    <td>{pokemon.name.english}</td>
    <td>{pokemon.type.join(", ")}</td>
    {/* NEW table cell */}
    <td>
      <Button
        size="small"
        variant="contained"
        color="primary"
        onClick={() => onClick(pokemon)}
      >
        More Information
      </Button>
    </td>
  </tr>
);

Re-compose it into pokemon with:

import { PokemonRow } from "../components/PokemonRow";

Create a Link in PokemonRow:

import { Button } from "@mui/material";
import Link from "next/link";

export const PokemonRow = ({ pokemon, onClick }) => (
  <>
    <tr key={pokemon.id}>
      {/* HERE */}
      <td>
        <Link href={`/pokemon/${pokemon.id}`}>{pokemon.name.english}</Link>
      </td>
      <td>{pokemon.type.join(", ")}</td>
      <td>
        <Button
          variant="contained"
          color="primary"
          onClick={() => onClick(pokemon)}
        >
          More Information
        </Button>
      </td>
    </tr>
  </>
);

Create pages/pokemon/[id].js:

import { useRouter } from "next/router";

import styled from "@emotion/styled";

const PageContainer = styled.div`
  margin: auto;
  width: 800px;
  padding-top: 1em;
`;

export default function SinglePokemon({ pokemon }) {
  const router = useRouter();

  return <PageContainer>{router.query.id}</PageContainer>;
}

Test the links.

When the app sees a url such as /pokemon/2 it will map the number to query parameter called id and invoke this page.

We need to make the pokemon collection available to this component in order to filter on them and display the info for a single Pokemon.

We will use React Context.

Context

We will use React Context to make the pokemon collection available to any component in our app without prop drilling.

Create src/PokemonContext.jsx:

import React from "react";

const PokemonContext = React.createContext({});

export default PokemonContext;

Import it into the pokemon page:

import PokemonContext from "../src/PokemonContext";

And enclose the entire component with the Context.Provider method:

// prettier-ignore
return (
  <PokemonContext.Provider
    value={{
      filter,
      pokemon,
      filterSet,
      pokemonSet,
      selectedPokemon,
      selectedPokemonSet,
    }}
  >
    <PageContainer>
      <CssBaseline />
      <Title>Pokemon Search</Title>
      <TwoColumnLayout>
         ...
      </TwoColumnLayout>
    </PageContainer>
  </PokemonContext.Provider>
);

Let's test it.

Create components/PokemonFilter.jsx:

import React, { useContext } from "react";
import { TextField } from "@mui/material";
import styled from "@emotion/styled";
import PokemonContext from "../src/PokemonContext";

const StyledTextField = styled(TextField)`
  width: 100%;
  padding: 0.2rem;
  font-size: large;
  margin: 2rem 0;
`;

export const PokemonFilter = () => {
  const { filter, filterSet } = useContext(PokemonContext);

  return (
    <StyledTextField
      variant="standard"
      type="search"
      value={filter}
      label="Filter Pokemon"
      onChange={(event) => filterSet(event.target.value)}
    />
  );
};

And compose it in pokemon.js:

import { PokemonFilter } from "../components/PokemonFilter";
// prettier-ignore
<div>
  <PokemonFilter />
  <table width="100%">
    ...
  </table>
</div>

Note: the necessary filter and filterSet props are available in the filter component via Context, not props:

This works for PokemonFilter but not for [id].js because it is a page, not a child of pokemon.js. We need to set the Context higher up in the app to make the data available to all pages.

Since NextJS follows a different paradigm and structures the application into individual pages, there is no index.js like what we saw in the Create React App. It has been abstracted away.

The work around for this is to use the custom app in pages/_app.jsx

import React from "react";
import "../styles/globals.css";
import type { AppProps } from "next/app";
import Layout from "../components/layout";
import PokemonContext from "../src/PokemonContext";

export default function App({ Component, pageProps }: AppProps) {
  const [pokemon, pokemonSet] = React.useState([]);

  React.useEffect(() => {
    fetch("/pokemon.json")
      .then((resp) => resp.json())
      .then((data) => pokemonSet(data));
  }, []);

  return (
    <PokemonContext.Provider
      value={{
        pokemon,
        pokemonSet,
      }}
    >
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </PokemonContext.Provider>
  );
}

Once we have done this we need to change the pokemon page.

  1. import useContext
  2. Take the initial pokemon from PokemonContext
  3. Remove the useEffect call
import React, { useContext } from "react";

// get the pokemon from context
// const [pokemon, pokemonSet] = React.useState(null);
const { pokemon, pokemonSet } = useContext(PokemonContext);
...
// React.useEffect(() => {
//   fetch("/pokemon.json")
//     .then((resp) => resp.json())
//     .then((data) => pokemonSet(data));
// }, []);
...
// <PokemonContext.Provider
//   value={{
//     filter,
//     pokemon,
//     filterSet,
//     pokemonSet,
//     selectedPokemon,
//     selectedPokemonSet,
//   }}
// >

Ensure we can see the pokemon collection in [id].js:

import { useRouter } from "next/router";
import { useContext } from "react";
import PokemonContext from "../../src/PokemonContext";
import styled from "@emotion/styled";

const PageContainer = styled.div`
  margin: auto;
  width: 800px;
  padding-top: 1em;
`;

export default function SinglePokemon() {
  const { pokemon } = useContext(PokemonContext);
  const router = useRouter();

  return (
    <PageContainer>
      <pre>{JSON.stringify(pokemon[router.query.id], null, 2)}</pre>
    </PageContainer>
  );
}

Let's filter for the pokemon with the id and view it:

export default () => {
  const { pokemon } = useContext(PokemonContext);
  const router = useRouter();
  const currpokemon = pokemon.find((p) => p.id === parseInt(router.query.id));

  return (
    <PageContainer>
      {router.query.id}, {JSON.stringify(currpokemon, null, 2)}
    </PageContainer>
  );
};

Use MUI to style the content.

import { useContext } from "react";
import { useRouter } from "next/router";
import PokemonContext from "../../src/PokemonContext";
import styled from "@emotion/styled";

import {
  CssBaseline,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableRow,
} from "@mui/material";

const PageContainer = styled.div`
  margin: auto;
  width: 800px;
  padding-top: 1em;
`;

const TypeHeader = styled.span`
  font-weight: bold;
`;

export default function SinglePokemon() {
  const { pokemon } = useContext(PokemonContext);
  const router = useRouter();
  const currpokemon = pokemon.find((p) => p.id === parseInt(router.query.id));

  return (
    <PageContainer>
      <CssBaseline />
      <div>
        {currpokemon && (
          <>
            <h1>{currpokemon.name.english}</h1>
            <p>
              <TypeHeader>Type:</TypeHeader> {" " + currpokemon.type.join(", ")}
            </p>
            <Table>
              <TableHead>
                <TableRow>
                  <TableCell>Attribute</TableCell>
                  <TableCell>Value</TableCell>
                </TableRow>
              </TableHead>
              <TableBody>
                {Object.keys(currpokemon.base).map((key) => (
                  <TableRow key={key}>
                    <TableCell>{key}</TableCell>
                    <TableCell>{currpokemon.base[key]}</TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
            <pre>
              <code>{JSON.stringify(currpokemon, null, 2)}</code>
            </pre>
          </>
        )}
      </div>
    </PageContainer>
  );
}

Test the filter. It needs access to filter and filterSet. We'll add it to _app.js along with Material UI's ThemeProvider context provider and the CssBaseLine MUI component.

In _app.js:

import React from "react";
import PokemonContext from "../src/PokemonContext";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";

const darkTheme = createTheme({
  palette: {
    mode: "dark",
  },
});

export default function App({ Component, pageProps }) {
  const [pokemon, pokemonSet] = React.useState([]);
  const [filter, filterSet] = React.useState("");
  React.useEffect(() => {
    fetch("/pokemon.json")
      .then((resp) => resp.json())
      .then((data) => pokemonSet(data));
  }, []);

  return (
    <ThemeProvider theme={darkTheme}>
      <PokemonContext.Provider
        value={{
          pokemon,
          pokemonSet,
          filter,
          filterSet,
        }}
      >
        <CssBaseline />
        <Component {...pageProps} />
      </PokemonContext.Provider>
    </ThemeProvider>
  );
}
export default function Pokemon() {
  // const [filter, filterSet] = React.useState("");
  // const [pokemon, pokemonSet] = React.useState(null);
  const { pokemon, pokemonSet, filter, filterSet } = useContext(PokemonContext);

Server Side Rendering

Save and commit all files. Create a new branch.

In a SPA the page is rendered on the client (browser). In SSR the page is generated on the server when the server gets a request. This allows for superior search engine optimization.

To use SSR for a page, we need to export an async getServerSideProps function. This async function is called each time a request is made for the page.

Add to server-side-pokemon.js:

export async function getServerSideProps() {
  const response = await fetch("http://localhost:3000/pokemon.json");
  const pokemon = await response.json();
  return {
    props: {
      pokemon,
    },
  };
}

Any exported getServerSideProps function is called on the server before the page is rendered and the server will create properties that are then sent to the page component.

We no longer need pokemon from Context. Remove Context, the provider, and pass the pokemon into the component:

import React from "react";
// import PokemonContext from "../src/PokemonContext";
import styled from "@emotion/styled";
import { CssBaseline } from "@mui/material";
import { PokemonRow } from "../components/PokemonRow";
import { PokemonInfo } from "../components/PokemonInfo";
import { PokemonFilter } from "../components/PokemonFilter";

const Title = styled.h1`
  text-align: center;
`;
const PageContainer = styled.div`
  margin: auto;
  width: 800px;
  padding-top: 1em;
`;
const TwoColumnLayout = styled.div`
  display: grid;
  grid-template-columns: 80% 20%;
  grid-column-gap: 1rem;
`;
const Input = styled.input`
  width: 100%;
  padding: 0.2rem;
  font-size: large;
`;

export async function getServerSideProps() {
  const response = await fetch("http://localhost:3000/pokemon.json");
  const pokemon = await response.json();
  // send the list of pokemon to the component
  return {
    props: {
      pokemon,
    },
  };
}

// NEW
export default function Pokemon({ pokemon }) {
  const [filter, filterSet] = React.useState("");
  // const { pokemon, pokemonSet } = useContext(PokemonContext);
  const [selectedPokemon, selectedPokemonSet] = React.useState(null);

  if (!pokemon) {
    return <div>Loading data</div>;
  }

  return (
    // <PokemonContext.Provider
    //   value={{
    //     filter,
    //     pokemon,
    //     filterSet,
    //     pokemonSet,
    //     selectedPokemon,
    //     selectedPokemonSet,
    //   }}
    // >
    <PageContainer>
      <Title>Pokemon Search</Title>
      <TwoColumnLayout>
        <div>
          <PokemonFilter filter={filter} filterSet={filterSet} />
          <table width="100%">
            <tbody>
              {pokemon
                .filter(({ name: { english } }) =>
                  english
                    .toLocaleLowerCase()
                    .includes(filter.toLocaleLowerCase())
                )
                .slice(0, 20)
                .map((pokemon) => (
                  <PokemonRow
                    key={pokemon.id}
                    pokemon={pokemon}
                    onClick={(pokemon) => selectedPokemonSet(pokemon)}
                  />
                ))}
            </tbody>
          </table>
        </div>
        {selectedPokemon && <PokemonInfo {...selectedPokemon} />}
      </TwoColumnLayout>
    </PageContainer>
    // </PokemonContext.Provider>
  );
}

View page source. All the the data is in a script tag.

Here, we are only generating the HTML on the server only once for the requested page so that search engines see the proper HTML while the application will behave exactly the same in the browser.

React needs the data in order to create a new tree that matches the tree on the page.

Since the server will return the proper HTML for the page, the user will no longer see a blank screen until all application resources are downloaded.

We can perform SSR on the individual views as well.

Create a new pages/ssr/ directory and save [id].js into it.

Create an alternate link that uses this path in PokemonRow.js:

<td>
  <Link href={`/pokemon/${pokemon.id}`}>{pokemon.name.english}</Link>
  <br />
  <Link href={`/ssr/${pokemon.id}`}>SSR {pokemon.name.english}</Link>
</td>

In ssr/[id].js:

// import { useContext } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
// import PokemonContext from "../../src/PokemonContext";
import {
  CssBaseline,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableRow,
} from "@mui/material";
import styled from "@emotion/styled";

const PageContainer = styled.div`
  margin: auto;
  width: 800px;
  padding-top: 1em;
`;
const TypeHeader = styled.span`
  font-weight: bold;
`;

// NEW
export async function getServerSideProps(context) {
  const response = await fetch("http://localhost:3000/pokemon.json");
  const allPokemon = await response.json();
  const currpokemon = allPokemon.find(
    (p) => p.id === parseInt(context.query.id)
  );
  return {
    props: {
      currpokemon,
    },
  };
}

export default ({ currpokemon }) => {
  const router = useRouter();
  // const { pokemon } = useContext(PokemonContext);
  // const currpokemon = pokemon.find((p) => p.id === parseInt(router.query.id));
  return (
    <PageContainer>
      <CssBaseline />

      <div>
        {currpokemon && (
          <>
            <h1>SSR: {currpokemon.name.english}</h1>
            <p>
              <TypeHeader>Type:</TypeHeader> {" " + currpokemon.type.join(", ")}
            </p>
            <Table>
              <TableHead>
                <TableRow>
                  <TableCell>Attribute</TableCell>
                  <TableCell>Value</TableCell>
                </TableRow>
              </TableHead>
              <TableBody>
                {Object.keys(currpokemon.base).map((key) => (
                  <TableRow key={key}>
                    <TableCell>{key}</TableCell>
                    <TableCell>{currpokemon.base[key]}</TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
            <Link href={`/`}>Home</Link>
            <pre>
              <code>{JSON.stringify(currpokemon, null, 2)}</code>
            </pre>
          </>
        )}
      </div>
    </PageContainer>
  );
};

At this point we can comment out the fetch in _app.js. Note the network tab. There is no fetch. The data is called on the server, not the front end.

Static Site Generation

For SSG NextJS uses getStaticPaths.

Ensure pokemon.json is in the src directory.

In [id].jsx:

export const getStaticPaths = async () => {
  const pokemon = require("../../src/pokemon.json");
  const paths = pokemon.map((p) => ({
    params: {
      id: p.id.toString(),
    },
  }));
  return {
    paths,
    fallback: false,
  };
};

export const getStaticProps = async (context) => {
  const allPokemon = require("../../src/pokemon.json");
  const currpokemon = allPokemon.find(
    (p) => p.id === parseInt(context.params.id)
  );
  return {
    props: { currpokemon },
  };
};

Note: at this point the filter is broken.

import React, { useContext } from "react";
import styled from "@emotion/styled";

// import PokemonContext from "../src/PokemonContext";

const Input = styled.input`
  width: 100%;
  padding: 0.2rem;
  font-size: large;
`;

export const PokemonFilter = ({ filter, filterSet }) => {
  // const { filter, filterSet } = useContext(PokemonContext);

  return (
    <Input
      type="text"
      value={filter}
      onChange={(event) => filterSet(event.target.value)}
    />
  );
};

And in pokemon.js:

<PokemonFilter filter={filter} filterSet={filterSet} />

In order to run SSG for the home page we need to make a few changes to pokemon.js:

import React, { useState } from "react";
// import PokemonContext from "../src/PokemonContext";
import styled from "@emotion/styled";
import { CssBaseline } from "@mui/material";

import allpokemon from "../public/pokemon.json";

import { PokemonRow } from "../components/PokemonRow";
import { PokemonFilter } from "../components/PokemonFilter";

// const PokemonInfo = ({ name: { english }, base }) => (
//   <div>
//     <h2>{english}</h2>
//     <table>
//       <tbody>
//         {Object.keys(base).map((key) => (
//           <tr key={key}>
//             map
//             <td>{key}</td>
//             <td>{base[key]}</td>
//           </tr>
//         ))}
//       </tbody>
//     </table>
//   </div>
// );

const Title = styled.h1`
  text-align: center;
`;
const PageContainer = styled.div`
  margin: auto;
  width: 800px;
  padding-top: 1em;
`;
const TwoColumnLayout = styled.div`
  display: grid;
  grid-template-columns: 80% 20%;
  grid-column-gap: 1rem;
`;

export default function Pokemon() {
  const [filter, filterSet] = React.useState("");
  // const { pokemon, pokemonSet } = useContext(PokemonContext);
  const [pokemon, pokemonSet] = React.useState(allpokemon);
  const [selectedPokemon, selectedPokemonSet] = React.useState(null);

  if (!pokemon) {
    return <div>Loading data</div>;
  }

  return (
    <PageContainer>
      <CssBaseline />
      <Title>Pokemon Search</Title>
      <TwoColumnLayout>
        <div>
          <PokemonFilter />
          <table width="100%">
            <tbody>
              {pokemon
                .filter(({ name: { english } }) =>
                  english
                    .toLocaleLowerCase()
                    .includes(filter.toLocaleLowerCase())
                )
                .slice(0, 20)
                .map((pokemon) => (
                  <PokemonRow
                    key={pokemon.id}
                    pokemon={pokemon}
                    onClick={(pokemon) => selectedPokemonSet(pokemon)}
                  />
                ))}
            </tbody>
          </table>
        </div>
        {selectedPokemon && <PokemonInfo {...selectedPokemon} />}
      </TwoColumnLayout>
    </PageContainer>
  );
}

To see the individual pages for all the pokemon run a build.

Create an export script in package.json:

  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "export": "next export",
    "start": "next start"
  },

npm run build npm run export

cd into the new out directory and run:

PORT=6789 npx serve

API

https://github.com/vercel/next.js/tree/canary/examples/with-mongodb-mongoose

Compass

https://www.mongodb.com/try/download/compass

npm install

npm i mongodb mongoose

.env.local:

lib/dbConnect.js:

import mongoose from "mongoose";

/** 
Source : 
https://github.com/vercel/next.js/tree/canary/examples/with-mongodb-mongoose
**/
const MONGODB_URI = process.env.MONGODB_URI;

if (!MONGODB_URI) {
  throw new Error(
    "Please define the MONGODB_URI environment variable inside .env.local"
  );
}
/**
 * Global is used here to maintain a cached connection across hot reloads
 * in development. This prevents connections growing exponentially
 * during API Route usage.
 */
let cached = global.mongoose;

if (!cached) {
  cached = global.mongoose = { conn: null, promise: null };
}

async function dbConnect() {
  if (cached.conn) {
    return cached.conn;
  }

  if (!cached.promise) {
    const opts = {
      useNewUrlParser: true,
    };

    cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
      return mongoose;
    });
  }
  cached.conn = await cached.promise;
  return cached.conn;
}

export default dbConnect;

models/user.js

import mongoose from "mongoose";

const UserSchema = new mongoose.Schema({
  name: String,
  email: String,
});

module.exports = mongoose.models.User || mongoose.model("User", UserSchema);

pages/api/users.js

import dbConnect from "../../lib/dbConnect";
import User from "../../models/User";

export default async function handler(req, res) {
  const { method } = req;

  await dbConnect();

  switch (method) {
    case "GET":
      try {
        const users = await User.find({});
        res.status(200).json({ success: true, data: users });
      } catch (error) {
        res.status(400).json({ success: false });
      }
      break;
    case "POST":
      try {
        const user = await User.create(req.body);
        res.status(201).json({ success: true, data: user });
      } catch (error) {
        res.status(400).json({ success: false });
      }
      break;
    default:
      res.status(400).json({ success: false });
      break;
  }
}

Postman:

GET http://localhost:3000/api/users POST http://localhost:3000/api/users

curl --request POST \
  --url http://localhost:3000/api/users \
  --header 'Content-Type: application/json' \
  --data '{
 "name": "John Doe",
 "email": "john@doe.com"
}'

View the users on index.js:

export default function Home({ isConnected }) {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    getAsyncUsers();
  }, []);

  async function getAsyncUsers() {
    const response = await fetch("http://localhost:3001/api/users");
    const data = await response.json();
    console.log(data);
    setUsers(data.data);
  }

  ...

  <ul>
    {users.map((user) => (
      <li key={user._id}>{user.name}</li>
    ))}
  </ul>

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published