NavBar
jacobwicks
Posted on January 17, 2020
In this post we will make the NavBar
. In the next post we will make the Writing
component, where the user can write new cards. The NavBar
will let the user switch between Answering
cards and Writing
cards.
User Story
- The user thinks of a new card. The user opens the card editor. The user clicks the button to create a new card. The user writes in the card subject, question prompt, and an answer to the question. The user saves their new card.
This user story has a lot of things going on. To make this user story possible we will need to make new component where the user can write cards. The Writing
component will be a new 'scene' in the application. We will also need to give the user a way to get to the Writing scene.
Let's make a NavBar (Navigation Bar) component to give the user a way to choose between the two scenes. The two scenes will be the Writing
scene and the Answering
scene. The NavBar
will give the user a button to go to the Writing
scene. The NavBar
will also give the user a button to go to the Answering scene.
We have not made the NavBar
and the Writing
scene yet. The App
just shows the Answering
scene all the time. The NavBar
will go inside the App
. The Writing
scene will also go inside the App
. The App
will keep track of what to show the user. The NavBar
will tell the App
when the user wants to see a different scene.
In this post, we will
- Make a placeholder for the Writing component
- Write a typescript enum for the different scenes
- Change the
App
component to keep track of what scene to show the user - Make the
NavBar
component - Show the
NavBar
component to the user
By the end of this post the NavBar
component will show up on the screen and let the user choose between looking at the Answering component and the Writing component. In the next post we will actually make the real Writing component.
Placeholder for the Writing component
File: src/scenes/Writing/index.tsx
Will Match: src/scenes/Writing/complete/index-1.tsx
We haven't made Writing
yet. But we need to have something to show on the screen when we select Writing
. So we are going to make a placeholder component. This will just be a div with the word 'writing' in it. Because this is a placeholder we aren't going to take the time to write tests first.
The Writing
component is one of our 'scenes.' So its folder is src/scenes/Writing.
import React from 'react';
const Writing = () => <div>Writing</div>
export default Writing;
That's it!
Make the sceneTypes type
File: src/types.ts
Will Match: src/complete/types-5.ts
Add a new enum named 'SceneTypes' in src/types.ts
:
//defines the scenes that the user can navigate to
export enum SceneTypes {
//where the user answers questions
answering = "answering",
//where the user writes questions
writing = "writing"
};
Making the App Keep Track of the Scenes
Right now the App
just shows the Answering
scene all the time. But to make the user story possible we need to let the user choose the Writing
scene. We need to keep track of what scene the user is looking at. We are going to keep track of what scene the user is looking at inside the App
component. We'll keep track of what scene the user is looking at with useState
.
Features
- There is a NavBar
Choose Components
We'll use the custom NavBar
that we'll write later in this post
Decide What to Test
Let's test whether the NavBar
shows up.
App Test 1: Has the NavBar
File: src/App.test.tsx
Will Match: src/complete/test-5.tsx
Add a test that checks for the NavBar
. The NavBar
will have a Header
with the text 'Flashcard App.'
//shows the NavBar
it('shows the NavBar', () => {
const { getByText } = render(<App/>);
//the navbar has a header with the words "Flashcard App" in it
const navBar = getByText(/flashcard app/i);
//if we find the header text, we know the NavBar is showing up
expect(navBar).toBeInTheDocument();
});
Pass App Test 1: Has the NavBar
File: src/App.tsx
Will Match: src/complete/app-5.tsx
The App
component will keep track of which scene to show. We will use the useState()
hook from React to keep track of which scene to show. The NavBar
component will let the user choose the scene. The App
won't pass the test for showing the NavBar
until later in this post, after we have written the NavBar
and imported it into the App
.
Import the useState
hook from React.
import React, { useState } from 'react';
Import the SceneTypes
enum from types.
import { SceneTypes } from './types/';
Import the Writing
component.
import Writing from './scenes/Writing';
We haven't made the NavBar
yet, so we won't import it. After we make the NavBar
, we will come back to the App
and add the NavBar
to it.
Change the App
to this:
const App: React.FC = () => {
const [showScene, setShowScene] = useState(SceneTypes.answering);
return (
<CardProvider>
<StatsProvider>
{showScene === SceneTypes.answering && <Answering />}
{showScene === SceneTypes.writing && <Writing/>}
</StatsProvider>
</CardProvider>
)};
Here's why the code for the App
component looks so different now.
Curly Brackets and return
Before these changes the App function just returned JSX. The App had a 'concise body.' A function with a concise body only has an expression that gives the return value. But now we have added an expression before the expression that gives the return value. The new expression sets up useState
to track what scene to show. Because we have added an expression besides the return value to the function, we have to add curly brackets so the compiler knows to look for expressions and not just a return value. This is called a function with a 'block body.'
return()
This is the return method of your function. This tells the function to return the value inside the parentheses. The parentheses are not required. But if you don't have the parentheses, you have to start your JSX on the same line. So it would look like this:
//this would work
return <CardProvider>
<StatsProvider>
{showScene === SceneTypes.answering && <Answering />}
{showScene === SceneTypes.writing && <Writing/>}
</StatsProvider>
</CardProvider>;
But if you don't have parentheses, starting your JSX return value on the next line will not work.
//this won't work
return
<CardProvider>
<StatsProvider>
{showScene === SceneTypes.answering && <Answering />}
{showScene === SceneTypes.writing && <Writing />}
</StatsProvider>
</CardProvider>;
I think it is easier to read with the return value starting on the next line. So I put parentheses around the return value.
UseState
The useState hook gives us a place to keep a variable, and a function to change it.
const [showScene, setShowScene] = useState(SceneTypes.answering);
useState(SceneTypes.answering)
is the call to the useState
hook. SceneTypes.answering
is the starting value. TypeScript can figure out from this that the type of the variable showScene
will be SceneTypes
. You can also explicitly declare that you are using a type. Explicit declaration of a type on useState
looks like this:
useState<SceneTypes>(SceneTypes.answering);
const [showScene, setShowScene]
is the declaration of two const variables, showScene
and setShowScene
.
showScene
is a variable of type SceneTypes
. So showScene
will either be SceneTypes.answering
or SceneTypes.writing
. Remember when we wrote the enum SceneTypes
earlier? SceneTypes.answering
is the string 'answering' and SceneTypes.writing
is the string 'writing'. The variable showScene
can only equal one of those two strings.
setShowScene
is a function. It takes one argument. The argument that setShowScene
takes is of the type SceneTypes
. So you can only invoke setShowScene
with SceneTypes.answering
or SceneTypes.writing
. After you invoke setShowScene
, the value of showScene
will be set to the value that you passed to setShowScene
.
We will pass the function setShowScene
to the NavBar
. Nothing calls setShowScene
yet. But after we make the NavBar
, we will import it into the App
. Then we will pass the setShowScene
function to the NavBar
. The Navbar
will use setShowScene
to change the value of showScene
in App. When the value of showScene
changes, App will change what scene it shows to the user.
Conditional Rendering of Answering and Writing
Conditional Rendering is how you tell React that if some condition is true, you want to show this component to the user. Rendering a component means showing it to the user.
{showScene === SceneTypes.answering && <Answering />}
{}
: The curly brackets tell the compiler that this is an expression. The compiler will evaluate the expression to figure out what value it has before rendering it to the screen.
showScene === SceneTypes.answering
: this is an expression that will return a boolean value. It will return true or it will return false.
&&
: This is the logical AND operator. It tells the compiler that if the condition to the left of it is true, it should evaluate and return the expression to the right.
&& <Answering/>
: The logical && operator followed by the JSX for the Answering
component means 'if the condition to the left of &&
is true, show the Answering
component on the screen.'
There is one conditional rendering expression for each scene.
{showScene === SceneTypes.answering && <Answering />}
{showScene === SceneTypes.writing && <Writing/>}
This code means if showScene
is 'answering' show the Answering
component, and if showScene
is 'writing' show the Writing component.
You are done with the App
for now. The App
won't pass the test for the NavBar
until later in this post, after we have written the NavBar
and imported it into the App
.
The NavBar
Now we are ready to make the NavBar
. Once we have written the NavBar
, we will import it into the App
so it shows up on screen and lets the user choose which scene they want to see.
Features
- The user can click a button to go to the
Writing
scene - The user can click a button to go to the
Answering
scene
Choose Components
The NavBar
is a menu, so we will use the Menu component from Semantic UI React.
Decide What to Test
- menu
- header
- button loads
Answering
- button loads
Writing
Write the tests
File: src/components/NavBar/index.test.tsx
Will Match: src/components/NavBar/complete/test-1.tsx
Write a comment for each test.
//has a menu component
//has a header
//has a menu item button that loads the answering scene
//clicking answer invokes setShowScene
//has a menu item button that loads the writing scene
//clicking edit invokes setShowScene
Imports and afterEach
.
import React from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import NavBar from './index';
import { SceneTypes } from '../../types';
afterEach(cleanup);
Write a helper function to render the NavBar
. The helper function takes an optional prop function setShowScene
. We'll use this prop to make sure that the NavBar
calls the function setShowScene
when the user clicks the buttons.
const renderNavBar = (setShowScene?: (scene: SceneTypes) => void) => render(
<NavBar
showScene={SceneTypes.answering}
setShowScene={setShowScene ? setShowScene : (scene: SceneTypes) => undefined}
/>);
NavBar Test 1: Has a Menu
File: src/components/NavBar/index.tsx
Will Match: src/components/NavBar/complete/index-1.tsx
NavBar
takes two props. setShowScene
is a function that accepts a SceneType
as a parameter. showScene
is the SceneType
that is currently being shown.
Clicking the Menu Items will invoke setShowScene
with the appropriate SceneType
.
import React from 'react';
import { Menu } from 'semantic-ui-react';
import { SceneTypes } from '../../types';
const NavBar = ({
setShowScene,
showScene
}:{
setShowScene: (scene: SceneTypes) => void,
showScene: SceneTypes
}) => <Menu data-testid='menu'/>
export default NavBar;
Now NavBar
has a menu.
NavBar Test 2: Has a Header
File: src/components/NavBar/index.test.tsx
Will Match: src/components/NavBar/complete/test-2.tsx
If this weren't a tutorial, and you were designing the NavBar
yourself, maybe you wouldn't test if NavBar
has a header. You might decide that the header on the NavBar is not an important enough feature to test. The reason we are testing for the header is that the App
's test checks for the NavBar
by finding its header. So we want to be sure when we test NavBar
that it has a header, so that when we add it to the App
the tests will pass.
//has a header
it('has a header', () => {
const { getByText } = renderNavBar();
const header = getByText(/flashcard app/i);
expect(header).toBeInTheDocument();
});
Pass NavBar Test 2: Has a Header
File: src/components/NavBar/index.tsx
Will Match: src/components/NavBar/complete/index-2.tsx
Add the Menu.Item
header.
<Menu data-testid='menu'>
<Menu.Item header content='Flashcard App'/>
</Menu>
NavBar Test 3: Answering Button
File: src/components/NavBar/index.test.tsx
Will Match: src/components/NavBar/complete/test-3.tsx
//has a menu item button that loads the answering scene
it('has a button to get you to the answering scene', () => {
const { getByText } = renderNavBar();
const answering = getByText(/answer/i)
expect(answering).toBeInTheDocument();
});
Pass NavBar Test 3: Answering Button
File: src/components/NavBar/index.tsx
Will Match: src/components/NavBar/complete/index-3.tsx
The active
prop will highlight the Menu Item
when the expression evaluates to true. This Menu Item
will be active when the showScene
prop is SceneTypes.answering
.
<Menu data-testid='menu'>
<Menu.Item header content='Flashcard App'/>
<Menu.Item content='Answer Flashcards'
active={showScene === SceneTypes.answering}/>
</Menu>
NavBar Test 4: Clicking Answering Button
File: src/components/NavBar/index.test.tsx
Will Match: src/components/NavBar/complete/test-4.tsx
//clicking answer invokes setShowScene
it('clicking answer invokes setShowScene', () => {
const setShowScene = jest.fn();
const { getByText } = renderNavBar(setShowScene);
const answering = getByText(/answer/i)
fireEvent.click(answering);
expect(setShowScene).toHaveBeenLastCalledWith(SceneTypes.answering);
});
Pass NavBar Test 4: Clicking Answering Button
File: src/components/NavBar/index.tsx
Will Match: src/components/NavBar/complete/index-4.tsx
Add the onClick function to the Answering
button.
<Menu.Item content='Answer Flashcards'
active={showScene === SceneTypes.answering}
onClick={() => setShowScene(SceneTypes.answering)}/>
NavBar Tests 5-6: Writing Button
File: src/components/NavBar/index.test.tsx
Will Match: src/components/NavBar/complete/test-5.tsx
//has a menu item button that loads the writing scene
it('has a button to get you to the writing scene', () => {
const { getByText } = renderNavBar();
const writing = getByText(/edit/i)
expect(writing).toBeInTheDocument();
});
//clicking edit invokes setShowScene
it('clicking edit invokes setShowScene', () => {
const setShowScene = jest.fn();
const { getByText } = renderNavBar(setShowScene);
const writing = getByText(/edit/i)
fireEvent.click(writing);
expect(setShowScene).toHaveBeenLastCalledWith(SceneTypes.writing);
});
Pass NavBar Tests 5-6: Writing Button
File: src/components/NavBar/index.tsx
Will Match: src/components/NavBar/complete/index-5.tsx
<Menu data-testid='menu'>
<Menu.Item header content='Flashcard App'/>
<Menu.Item content='Answer Flashcards'
active={showScene === SceneTypes.answering}
onClick={() => setShowScene(SceneTypes.answering)}/>
<Menu.Item content='Edit Flashcards'
active={showScene === SceneTypes.writing}
onClick={() => setShowScene(SceneTypes.writing)}/>
</Menu>
Ok, now we have a NavBar
that passes all the tests! Let's import it into the App
and show it to the user.
Import NavBar into App
File: src/App.tsx
Will Match: src/complete/app-6.tsx
Now let's import the NavBar
into the App
. This will make App
pass the tests we wrote earlier. It will also make the NavBar
show up on screen. Once the user can see the NavBar
, they will be able to switch between the two scenes. The user will be able to look at the Answering
scene. The user will also be able to look at the Writing
scene. The Writing
scene that the user can see will be the placeholder that you wrote earlier in this post. In the next post we will make the actual Writing
component.
import NavBar from './components/NavBar';
Add the NavBar
component into the App
.
//rest of app component stays the same
return (
<CardProvider>
<StatsProvider>
//add the NavBar here
<NavBar setShowScene={setShowScene} showScene={showScene} />
{showScene === SceneTypes.answering && <Answering />}
{showScene === SceneTypes.writing && <Writing/>}
</StatsProvider>
</CardProvider>
)};
Save the App. Most of the tests will pass, but the snapshot test will fail because you have changed what shows up on the screen. Update the snapshot by pressing 'u'. Now all tests should pass.
Run the app with npm start
. You will see the Answering
scene with the NavBar
above it.
Click on 'Edit Flashcards'. You will see the placeholder Writing
scene.
Next Post
In the next post we will make the actual Writing
component.
Posted on January 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.