Building a Rick and Morty Dashboard with Next.js, Redux, and GraphQL

Victor Gago
7 min readJul 31, 2024

--

Taking a pause from my AZ-204 exam preparation, I decided to exercise some React skills by creating a Rick and Morty dashboard using Next.js, Redux, and GraphQL. This guide will cover each step of the process, from setting up your environment to integrating these technologies into a cohesive application:

A Rick and Morty character dashboard.

1. Setting Up Your Next.js Project

First, let’s set up a new Next.js project. If you don’t have Next.js installed, you can create a new project using the following command:

npx create-next-app@latest rick-and-morty-dashboard
cd rick-and-morty-dashboard

2. Installing Dependencies

For this project, you’ll need redux, react-redux, @reduxjs/toolkit, @apollo/client, and graphql. Install these dependencies with:

npm install @reduxjs/toolkit react-redux @apollo/client graphql

3. Configuring Redux

Create a Redux store and a slice for managing the state.

src/redux/store.ts

import { configureStore } from '@reduxjs/toolkit';
import charactersReducer from './reducers';

const store = configureStore({
reducer: {
characters: charactersReducer,
},
});

export default store;

src/redux/reducers.ts

import { createSlice } from '@reduxjs/toolkit';

const charactersSlice = createSlice({
name: 'characters',
initialState: { characters: [] },
reducers: {
setCharacters(state, action) {
state.characters = action.payload;
},
},
});

export const { setCharacters } = charactersSlice.actions;
export default charactersSlice.reducer;

4. Setting Up Apollo Client

Create an Apollo Client instance to interact with the GraphQL API.

src/lib/apolloClient.ts

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
uri: 'https://rickandmortyapi.com/graphql',
cache: new InMemoryCache(),
});

export default client;

5. Configuring the Layout

Ensure that the layout.tsx file includes the necessary HTML tags and wraps the application with both Redux and Apollo providers.

src/app/layout.tsx

"use client";

import React from 'react';
import { ApolloProvider } from '@apollo/client';
import { Provider } from 'react-redux';
import client from '../lib/apolloClient';
import store from '../redux/store';

const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<html>
<head>
<title>Rick and Morty Dashboard</title>
</head>
<body>
<ApolloProvider client={client}>
<Provider store={store}>
<div>{children}</div>
</Provider>
</ApolloProvider>
</body>
</html>
);
};

export default Layout;

6. Creating the Page Component

The page.tsx component will use Apollo Client to fetch data and Redux to manage the application state.

src/app/page.tsx

"use client";

import { useEffect } from 'react';
import { useQuery, gql } from '@apollo/client';
import { useDispatch, useSelector } from 'react-redux';
import { setCharacters } from '../redux/reducers';
import styles from './page.module.css';

const GET_CHARACTERS = gql`
query {
characters(page: 1) {
results {
id
name
image
status
species
}
}
}
`;

const HomePage = () => {
const dispatch = useDispatch();
const characters = useSelector((state: any) => state.characters.characters);
const { data, loading, error } = useQuery(GET_CHARACTERS);

useEffect(() => {
if (data) {
dispatch(setCharacters(data.characters.results));
}
}, [data, dispatch]);

if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;

return (
<div className={styles.container}>
<h1 className={styles.title}>Rick and Morty Characters</h1>
<ul className={styles.characterList}>
{characters.map((character: any) => (
<li key={character.id} className={styles.characterCard}>
<img src={character.image} alt={character.name} />
<h2>{character.name}</h2>
<p>{character.status} - {character.species}</p>
</li>
))}
</ul>
</div>
);
};

export default HomePage;

So far:

7. Enhancing the Application with Material-UI

In this section, we’ll enhance the styling of our Rick and Morty dashboard using Material-UI (MUI). We’ll set up a theme, add a theme toggler, replace HTML5 elements with MUI components, and use the sx prop for consistent styling.

7.1. Installing Material-UI

To start using Material-UI, we need to install the core and system packages:

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

7.2. Configuring the Theme

Material-UI allows you to customize the theme to fit your design preferences. Create a theme configuration file:

src/lib/theme.ts

import { ThemeOptions } from '@mui/material/styles';

const getTheme = (mode: 'light' | 'dark'): ThemeOptions => ({
palette: {
mode,
primary: {
main: '#08C952',
},
secondary: {
main: '#160440',
},
background: {
default: mode === 'dark' ? '#121212' : '#f5f5f5',
paper: mode === 'dark' ? '#1e1e1e' : '#ffffff',
},
text: {
primary: mode === 'dark' ? '#ffffff' : '#000000',
},
},
typography: {
h1: {
fontSize: '2rem',
fontWeight: 700,
},
body1: {
fontSize: '1rem',
},
},
});

export { getTheme };
export default getTheme;

7.3. Adding a Theme Toggler

To allow users to toggle between light and dark modes, create a theme toggler component:

src/components/ThemeToggle.tsx

import React from 'react';
import { IconButton, Tooltip } from '@mui/material';
import { Brightness7, Brightness4 } from '@mui/icons-material';

interface ThemeToggleProps {
mode: 'light' | 'dark';
toggleTheme: () => void;
}

const ThemeToggle: React.FC<ThemeToggleProps> = ({ mode, toggleTheme }) => {
const tooltipTitle = mode === 'light' ? 'Switch to Dark Mode' : 'Switch to Light Mode';

const icon = mode === 'light' ? <Brightness4 /> : <Brightness7 />;

return (
<Tooltip title={tooltipTitle}>
<IconButton color="inherit" onClick={toggleTheme}>
{icon}
</IconButton>
</Tooltip>
);
};

export default ThemeToggle;

7.4. Wrapping the Application with ThemeProvider

Update the Layout component to include the ThemeProvider and ThemeToggle:

src/app/layout.tsx

"use client";

import React from 'react';
import { ApolloProvider } from '@apollo/client';
import { Provider } from 'react-redux';
import { useApolloClient } from '../lib/apolloClient'; // Adjust path if needed
import store from '../redux/store'; // Import your Redux store

const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const client = useApolloClient(); // Hook to get Apollo Client instance

return (
<html>
<head>
<title>Rick and Morty Dashboard</title>
{/* You can include other meta tags and link tags here */}
</head>
<body>
<ApolloProvider client={client}>
<Provider store={store}>
<div>{children}</div>
</Provider>
</ApolloProvider>
</body>
</html>
);
};

export default Layout;

7.5. Replacing HTML5 Elements with Material-UI Components

Replace HTML5 elements with MUI components and apply styles using the sx prop for consistency:

src/app/page.tsx

"use client";

import React, { useState, useEffect, useMemo } from 'react';
import { useQuery, gql } from '@apollo/client';
import { useDispatch, useSelector } from 'react-redux';
import { setCharacters } from '../redux/reducers';
import { Container, Grid, Typography, Box, Paper, Toolbar, CssBaseline, ThemeProvider, createTheme, Fab } from '@mui/material';
import Search from '@/components/Search';
import Pagination from '@/components/Pagination';
import ThemeToggle from '@/components/ThemeToggle';
import { getTheme } from '../lib/theme';

// GraphQL query
const GET_CHARACTERS = gql`
query {
characters(page: 1) {
results {
id
name
image
status
species
}
}
}
`;

const HomePage = () => {
const [mode, setMode] = useState<'light' | 'dark'>('light');
const dispatch = useDispatch();
const characters = useSelector((state: any) => state.characters);
const { data, loading, error } = useQuery(GET_CHARACTERS);

useEffect(() => {
if (data) {
dispatch(setCharacters(data.characters.results));
}
}, [data, dispatch]);

useEffect(() => {
const savedMode = localStorage.getItem('theme') as 'light' | 'dark';
if (savedMode) {
setMode(savedMode);
}
}, []);

const toggleTheme = () => {
const newMode = mode === 'light' ? 'dark' : 'light';
setMode(newMode);
localStorage.setItem('theme', newMode);
};

const theme = useMemo(() => createTheme(getTheme(mode)), [mode]);

if (loading) return <Typography>Loading...</Typography>;
if (error) return <Typography>Error: {error.message}</Typography>;

return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box>
<Box
sx={{
backgroundImage: 'url(/background.jpg)',
backgroundSize: 'contain',
backgroundPosition: 'right center',
backgroundRepeat: 'no-repeat',
backgroundColor: '#252525',
width: '100%',
height: '25vh',
}}
>
<Container
sx={{
padding: '16px',
height: '25vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start'
}}
>
<Typography
variant="h1"
sx={{
color: '#fff',
fontSize: '2rem',
textAlign: 'left',
width: '100%'
}}
>
Rick and Morty Characters
</Typography>
</Container>
</Box>

<Toolbar
sx={(theme) => ({
width: '100%',
justifyContent: 'space-between',
padding: '12px',
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
marginBottom: '32px',
backgroundColor: theme.palette.background.paper
})}
>
<Container
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Search />
<Pagination />
</Container>
</Toolbar>

<Container>
<Grid container spacing={2} sx={{ marginBottom: '32px' }}>
{characters.map((character: any) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={character.id}>
<Paper
sx={{
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '100%',
overflow: 'hidden'
}}
>
<Box
sx={{
width: '100%',
height: '200px',
backgroundColor: '#e0e0e0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
position: 'relative'
}}
>
<img
src={character.image}
alt={character.name}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
position: 'absolute',
top: 0,
left: 0
}}
/>
</Box>
<Box
sx={{
padding: '12px',
}}
>
<Typography variant="h6">{character.name}</Typography>
<Typography variant="body2">
{character.status} - {character.species}
</Typography>
</Box>
</Paper>
</Grid>
))}
</Grid>

<Box
sx={{
marginBottom: '32px'
}}
>
<Pagination />
</Box>
</Container>
</Box>

<Fab
color="primary"
aria-label="theme toggle"
onClick={toggleTheme}
sx={{
position: 'fixed',
bottom: '16px',
right: '16px',
}}
>
<ThemeToggle mode={mode} toggleTheme={toggleTheme} />
</Fab>
</ThemeProvider>
);
};

export default HomePage;

By following these steps, we’ve successfully integrated Material-UI into the dashboard, resulting in a responsive and modern UI with a customizable theme, consistent styling, and improved user experience.

Conclusion

By combining Next.js, Redux, and GraphQL, I created a Rick and Morty characters dashboard that showcases how these technologies can work seamlessly together to build a modern web application. This project not only demonstrates the practical application of server-side rendering, state management, and efficient data fetching but also offers a starting point for those interested in exploring similar integrations.

Feel free to explore the complete code on GitHub. I hope this project inspires you to experiment with these technologies and tailor them to your own needs. Happy coding!

--

--

Victor Gago
Victor Gago

No responses yet