Creating an application
By convention, applications are created in the apps directory. Let's create one there.
Create a React app
shaper
? Which plugin would you like to run? React
? Which generator would you like to run? app
? Application name? movie-magic
? Parent directory? apps
? Package name used for publishing? @movie-magic/movie-magic
# Add a dependency to ui-lib in apps/movie-magic/package.json
"dependencies": {
"@movie-magic/ui-lib": "*",
...
}
# In the root directory, run:
npm install
# To make sure that everything is set up correctly, run a build
npm run build
# Finally, run the app from the root directory
npm run dev
Point your browser to http://localhost:3000
. You should see a placeholder home
page with just a header.
The goal of this section is to show a list of top 10 movies on this page.
Before we go forward, let's commit the generated code:
# Commit
git add .
git commit -m "Added movie-magic app"
Create a Movie model
Let's start by creating a TypeScript definition for a movie. Add a file called
Movie.ts
under apps/movie-magic/src/models
with the following content.
export interface Movie {
name: string;
year: number;
rating: number;
}
Add another file called index.ts
to easily access the movie definition. This
is called barreling.
export * from './Movie';
Create a MovieList component
Now we will create a MovieList
component that receives a list of movies and
displays it. Such components are called presentational components - they don't
worry about how the data was obtained, their job is to simply render it.
The process of generating the <MovieList>
component is exactly the same as
that for the <Button>
component from the last section. Follow the steps below:
shaper
? Which plugin would you like to run? React
? Which generator would you like to run? component
? Component name? MovieList
? Which workspace should this go to? apps/movie-magic
? Parent directory within workspace? src/components/MovieList
# A placeholder MovieList component has been created for you.
# Also a placeholder Storybook story has been created for you.
# Let's implement MovieList interactively using Storybook.
npm run storybook
# Point your browser to http://localhost:6006.
# Storybook shows a placeholder implementation of MovieList.
Implement MovieList
We are now ready to implement MovieList
. Overwrite the placeholder
implementation with the one below.
import { Button } from '@movie-magic/ui-lib';
import { Movie } from '../../models';
interface MovieListProps {
movies: Array<Movie>;
}
export function MovieList({ movies }: MovieListProps) {
return (
<table data-testid="movie-table">
<thead>
<tr>
<th className="text-center">Rank</th>
<th>Name</th>
<th className="text-center">Year</th>
<th className="text-center">Rating</th>
<td></td>
</tr>
</thead>
<tbody>
{movies.map((movie, index) => (
<tr key={movie.name}>
<td className="text-center">{index + 1}</td>
<td>{movie.name}</td>
<td className="text-center">{movie.year}</td>
<td className="text-center">{movie.rating.toFixed(1)}</td>
<td className="text-center">
<Button>Watch</Button>
</td>
</tr>
))}
</tbody>
</table>
);
}
We also need to modify the story to supply a list of movies. Overwrite the story with the code below:
import { Story, Meta } from '@storybook/react';
import { MovieList } from './MovieList';
export default {
title: 'Components/MovieList',
component: MovieList,
} as Meta;
const Template: Story = (args) => (
<div className="card p-2">
<MovieList movies={args.movies} />
</div>
);
export const MovieListStory = Template.bind({});
MovieListStory.storyName = 'MovieList';
MovieListStory.args = {
movies: [
{
name: 'The Shawshank Redemption',
year: 1994,
rating: 9.3,
},
{
name: 'The Godfather',
year: 1972,
rating: 9.2,
},
{
name: 'The Godfather: Part II',
year: 1974,
rating: 9.0,
},
],
};
Here's a snapshot of the updated Storybook interface.
Implement unit tests
The final step is to implement a unit test for MovieList
. Update the
placeholder test with the code below.
import { render, screen } from '../../test/test-utils';
import { MovieList } from './MovieList';
const movies = [
{
name: 'The Shawshank Redemption',
year: 1994,
rating: 9.3,
},
{
name: 'The Godfather',
year: 1972,
rating: 9.2,
},
{
name: 'The Godfather: Part II',
year: 1974,
rating: 9.0,
},
];
describe('<MovieList />', () => {
test('renders correctly', async () => {
render(<MovieList movies={movies} />);
// expect 3 movies
const movieTable = await screen.findByTestId('movie-table');
const movieRows = movieTable.querySelectorAll('tbody tr');
expect(movieRows.length).toBe(movies.length);
});
});
Workaround for Jest issue
There appears to be an issue with Jest when coverage is turned on. The tests will pass, but you will see the following error when Jest is collecting coverage information:
"ERROR: Jest worker encountered 3 child process exceptions, exceeding retry limit"
To work around this issue disable coverage for the movie-magic app. Edit
apps/movie-magic/package.json and delete the --coverage
option for jest.
Run the tests from the root directory. All tests should pass.
npm test
MovieList is now fully implemented, let's commit the code:
# Commit
git add .
git commit -m "Added MovieList"
Mock API request
Now that we have implemented the MovieList
component, we need to think about
how to fetch the list of top 10 movies and feed it to `MovieList. To do this, we
will use a tool called Mock Service Worker. MSW intercepts
API requests at the network level and returns mock responses. This allows us to
start testing our front-end without having to wait for the real API to be ready.
Add the following file containing movie data under the mocks
directory:
import { Movie } from '../models';
export const mockMovies: Array<Movie> = [
{
name: 'The Shawshank Redemption',
year: 1994,
rating: 9.3,
},
{
name: 'The Godfather',
year: 1972,
rating: 9.2,
},
{
name: 'The Godfather: Part II',
year: 1974,
rating: 9.0,
},
{
name: 'The Dark Knight',
year: 2008,
rating: 9.0,
},
{
name: '12 Angry Men',
year: 1957,
rating: 8.9,
},
{
name: "Schindler's List",
year: 1993,
rating: 8.9,
},
{
name: 'The Lord Of The Rings: The Return Of The King',
year: 2003,
rating: 8.9,
},
{
name: 'Pulp Fiction',
year: 1994,
rating: 8.9,
},
{
name: 'The Good, The Bad And The Ugly',
year: 1966,
rating: 8.8,
},
{
name: 'The Lord Of The Rings: The Fellowship Of The Rings',
year: 2001,
rating: 8.8,
},
];
Replace the placeholder handler in handlers.ts
with top-10-movies
handler:
import { rest } from 'msw';
import { MOCK_API_URL } from './constants';
import { mockMovies } from './mockMovies';
export const handlers = [
rest.get(`${MOCK_API_URL}/top-10-movies`, (req, res, ctx) => {
return res(ctx.status(200), ctx.json(mockMovies));
}),
];
This completes the implementation of the mock API handler. We will call this API from the client using a fetch hook. Before we do that, let's commit our code:
# Commit
git add .
git commit -m "Added mock API for fetching top 10 movies"
Create a hook to fetch movies
Create a file called useMovies.ts
under the HomePage folder to fetch top
10 movies.
import * as React from 'react';
import { Movie } from '../../models';
/**
* Hook to fetch movies
*/
export function useMovies() {
const apiUrl = import.meta.env.VITE_API_URL;
const failMessage = 'Failed to get movies';
const [isLoading, setIsLoading] = React.useState(false);
const [isError, setIsError] = React.useState(false);
const [error, setError] = React.useState<Error>();
const [movies, setMovies] = React.useState<Array<Movie>>([]);
React.useEffect(() => {
const fetchMovies = async () => {
try {
setIsLoading(true);
const response = await fetch(`${apiUrl}/top-10-movies`);
if (!response.ok) {
setIsError(true);
setError(new Error(`${failMessage} (${response.status})`));
setIsLoading(false);
return;
}
const movies = await response.json();
setMovies(movies);
setIsLoading(false);
} catch (error) {
setIsError(true);
setError(error instanceof Error ? error : new Error(failMessage));
setIsLoading(false);
}
};
fetchMovies();
}, [apiUrl]);
return { isLoading, isError, error, movies };
}
Create a container to fetch movies
Create a file called MovieListContainer.tsx
under the HomePage folder.
This component fetch movies using the useMovies
hook we created above. Once
the movies are received, it renders them using the MovieList
component we
created earlier.
import * as React from 'react';
import { MovieList } from '../../components';
import { useMovies } from './useMovies';
export function MovieListContainer() {
const { isLoading, isError, error, movies } = useMovies();
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <h1 className="text-2xl font-semibold mb-2">{error?.message}</h1>;
}
return (
<React.Fragment>
<h1 className="text-2xl font-semibold mb-2">Top 10 Movies Of All Time</h1>
<MovieList movies={movies} />
</React.Fragment>
);
}
Add container to HomePage
Finally, add MovieListContainer
to HomePage
to render the list of movies in
the home page.
import * as React from 'react';
import { Header } from '../../components';
import { MovieListContainer } from './MovieListContainer';
export function HomePage() {
return (
<React.Fragment>
<Header />
<div className="p-3">
<div className="card p-3">
<MovieListContainer />
</div>
</div>
</React.Fragment>
);
}
Run the app:
npm run dev
Point your browser to http://localhost:3000
. You should see the updated home
page with the movie list.
Commit your code
# Commit
git add .
git commit -m "Added movie list to the home page"
Congratulations! You have now learned how to use Code Shaper using off-the-shelf generators. Now let's learn how to write your own custom generator