Phresh Frontend - Bootstrapping A React App
Welcome to Part 13 of Up and Running with FastAPI. If you missed part 12, you can find it here .
This series is focused on building a full-stack application with the FastAPI framework. The app allows users to post requests to have their residence cleaned, and other users can select a cleaning project for a given hourly rate.
Up And Running With FastAPI
In the last post, we gave users the ability to mark cleaning jobs as completed and evaluate the cleaner’s work. We also implemented a ratings system used to display aggregate evaluations for cleaners.
With that in place, we’re ready to start building out our frontend to consume the FastAPI backend we’ve constructed.
Environment and Setup
Alright, let’s get an app up and running.
We’ll leverage React for our frontend, and offload a majority of our component creation work to the Elastic UI Framework .
The quickest way to bootstrap a react app is to use create-react-app
and the quickest way to use create-react-app
is with npx
.
So head to the terminal and punch out:
npx create-react-app phresh-frontend
It’ll take a little while to get started. Grab a coffee ☕️ or something and we’ll get rocking when you’re back.
Once your app is done building, we’ll need to do a few things.
We’re also going to be installing additional libraries for routing, styling, and state management, so let’s go into those briefly.
The first thing to touch on is how we’ll be managing state in React.
With the release of version 16.8, React introduced hooks - the newest way to handle state in function-based components. I’m all in on hooks, though I haven’t been fully converted to using the Context API in it’s entirety yet. I’ve still found Redux to be more manageable as my application grows and has better tooling (maybe I’m just biased). What that means is we’ll be using nothing but functions to create our React app and handling business logic in Redux actions.
It’s ok if you’re unfamiliar with those concepts. We’ll get to each one in turn.
Regardless, we’ll need the redux
, react-redux
, redux-thunk
, and axios
libraries for our global state and http request system, so that’s where those are coming from.
Second, we’ll deal with styling.
Styling react components is a sensitive subject, and everyone has their opinions. For this project, we’ll be using styled-components , a CSS-in-JS variant that has worked well for me professionally. Much of the default styling provided by elastic-ui
will be good enough, but some customization is unavoidable. For our needs here, we’ll install the styled-components
, @elastic/eui
, @elastic/datemath
, and moment
libraries.
And lastly, routing.
Navigation will be handled by the @next
version of React Router - though by the time you’re reading this article it may be the stable version - >= 6.0.0! This “latest and greatest” version couples the best of reach-router
and react-router
into a single library that I feel is here to stay. This is where the history
and react-router-dom@next
libraries come into play.
Again, don’t worry if you’re unfamiliar with any of this. All in due time.
Oh, and just for good measure let’s include an animation library that I’ve become particularly fond of - framer-motion
.
For now, go ahead and run the following:
cd phresh-frontend
yarn add styled-components react-helmet history react-router-dom@next @elastic/eui @elastic/datemath moment redux react-redux redux-thunk axios framer-motion
This will also take a few moments, so be patient. Once that’s finished, we can start writing some code.
Let’s take a stupid-quick tour of our application while we wait.
|-- node_modules
|-- public
|-- src
| |-- App.css
| |-- App.js
| |-- App.test.js
| |-- index.css
| |-- index.js
| |-- logo.svg
| |-- serviceWorker.js
| |-- setupTests.js
|-- package.json
|-- .gitignore
|-- READMD.md
|-- yarn.lock
The public
directory holds a single index.html
file and some static assets. When our React app is built, it will be bundled and served in this index.html
file to be rendered in the browser.
We also have a src
directory with an index.js
file and an App.js
file. That’s where most of the magic is happening.
To see it action, we run yarn start
and check out localhost:3000
.
And we see the standard React loading screen that everyone sees.
Now we’re ready to get started with some simple structuring.
Project Structure
Organizing files and folders is another sensitive matter. So if you hate my system, I’m not offended.
In the src
directory, create new directories for assets
, components
, hooks
, redux
, services
, and utils
. Create a config.js
file in the src
directory as well.
touch src/config.js
mkdir src/assets src/components src/hooks src/redux src/services src/utils
mkdir src/assets/img src/assets/css src/assets/json
touch src/components/index.js
mkdir src/components/App
Go ahead and delete the logo.svg
, serviceWorker.js
, setupTests.js
, index.css
, App.css
, and App.test.js
files and move the App.js
file into it’s own component folder.
Also remove all of the contents in App.js
, and add a simple h1
so it looks like this:
import React from "react"
export default function App() {
return (
<div className="App">
<h1>Phresh</h1>
</div>
)
}
Now we have a file structure resembling the following:
|-- node_modules
|-- public
|-- src
| |-- assets
| |-- img
| |-- components
| |-- App
| |-- App.js
| |-- index.js
| |-- hooks
| |-- redux
| |-- services
| |-- utils
| |-- config.js
| |-- index.js
|-- package.json
|-- .gitignore
|-- READMD.md
|-- yarn.lock
Also everything should be broken. Our exports are all messed up, so let’s remedy that. In the components/index.js
file, add the following:
export { default as App } from "./App/App"
Now why do we do that? Well as the application grows, it can be cumbersome to locate every component. By exporting all components from the components/index.js
file, we can import every component directly from this file.
We’ll follow that exact pattern a lot, so get familiar with it.
Then, update the the src/index.js
file like so:
import React from "react"
import ReactDOM from "react-dom"
import { App } from "./components"
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
)
Head back to the browser, hit refresh and we should see a super simple page with the word Phresh
on it. We’re ready to get going.
Getting Familiar with Elastic UI and Styled Components
With a barebones setup in place, we can move on to building our design system with elastic-ui
and styled-components
.
We’ll begin with a standard component - Layout.js
.
Create the folder and file like so:
mkdir src/components/Layout
touch src/components/Layout/Layout.js
The Layout
component will render other components inside of it, so it’s a good place to add any global styles, themes, and other meta data inside our application.
Start by importing the standard React imports needed for a component and adding the following to the Layout.js
file:
import React from "react"
import { Helmet } from "react-helmet"
import styled from "styled-components"
const StyledLayout = styled.div`
width: 100%;
max-width: 100vw;
min-height: 100vh;
background: rgb(224, 228, 234);
display: flex;
flex-direction: column;
`
const StyledMain = styled.main`
min-height: 100vh;
display: flex;
flex-direction: column;
`
export default function Layout({ children }) {
return (
<React.Fragment>
<Helmet>
<meta charSet="utf-8" />
<title>Phresh Cleaners</title>
<link rel="canonical" href="https://phreshcleaners.com" />
</Helmet>
<StyledLayout>
<StyledMain>{props.children}</StyledMain>
</StyledLayout>
</React.Fragment>
)
}
Notice a few things here. We’re importing Helmet
from react-helmet
and adding metadata to it just like we would to the <Head>
of an html file. React Helmet is a package from NFL engineering that makes it easy to update metadata just like that.
We’ve also imported styled components and created our first couple styled components. Instead of creating a css file, we simply create a React Component called StyledLayout
with the styled.div
function. This might look funky to you, since it doesn’t resemble functions we’ve seen previously. Instead of calling the function with parenthesis, we call it with backticks - ES6 template literals that provide additional flexibility when composing strings.
We then create and export our Layout function which takes in some props and renders its contents. Another new thing we see here is this React.Fragment
component. Traditionally, we weren’t allowed to render components as siblings of each other. We were only allowed to return a single component. By using a fragment, we don’t have add an arbitrary div
here, and can instead render as many sibling elements as we see fit.
This Layout component simply takes in children as a prop, and renders them inside the StyledMain
component. Nothing magical here. Let’s export it from our components/index.js
file.
export { default as App } from "./App/App"
export { default as Layout } from "./Layout/Layout"
Now, in the App.js
file, import the Layout component and wrap the h1 with our styled Layout
.
import React from "react"
import { Layout } from "../../components"
export default function App() {
return (
<Layout>
<h1>Phresh</h1>
</Layout>
)
}
Tada! Look at that. All components nested inside our Layout component are rendered as children, and we see the nice background styles applied to our component.
Now let’s bring elastic-ui
into the mix.
Back in our Layout.js
component, we’ll bring in the light theme from elastic-ui
and add it to the ThemeProvider
given to us by styled-components
. On top of that, we’re going to create a css file to override any global styles we don’t like and name it src/assets/css/override.css
. We’ll also want to import the recommended elastic-ui
fonts, so create another file in src/assets/css
called fonts.css
:
touch src/assets/css/override.css src/assets/css/fonts.css
And add the following:
@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:400,400i,700,700i');
@import url('https://rsms.me/inter/inter-ui.css');
Then in the Layout.js
file:
import React from "react"
import { Helmet } from "react-helmet"
import styled, { ThemeProvider } from "styled-components"
import euiVars from "@elastic/eui/dist/eui_theme_light.json"
import "@elastic/eui/dist/eui_theme_light.css"
import "../../assets/css/fonts.css"
import "../../assets/css/override.css"
const customTheme = {
...euiVars,
euiTitleColor: "dodgerblue",
}
const StyledLayout = styled.div`
width: 100%;
max-width: 100vw;
min-height: 100vh;
background: rgb(224, 228, 234);
display: flex;
flex-direction: column;
`
const StyledMain = styled.main`
min-height: calc(100vh - ${(props) => props.theme.euiHeaderHeight} - 1px);
display: flex;
flex-direction: column;
& h1 {
color: ${(props) => props.theme.euiTitleColor};
}
`
export default function Layout({ children }) {
return (
<React.Fragment>
<Helmet>
<meta charSet="utf-8" />
<title>Phresh Cleaners</title>
<link rel="canonical" href="https://phreshcleaners.com" />
</Helmet>
<ThemeProvider theme={customTheme}>
<StyledLayout>
<StyledMain>{children}</StyledMain>
</StyledLayout>
</ThemeProvider>
</React.Fragment>
)
}
We’ve imported our own css file - fonts.css
- and the light theme css file from elastic-ui
. We also imported a ~700 line JSON file of variables, and added a single override to it: euiTitleColor
. Instead of the black text default, we opted for dodgerblue (don’t worry, you don’t have to keep that - it’s just to demonstrate how this works).
Feel free to define any other overrides that match your preferences. We pass that new custom theme to our ThemeProvider
component and wrap our Layout
with it, ensuring that all children have access to those variables throughout the app. If you want to see the list of variables available to override, check them out here .
To access those variables in any styled component, we interpolate a fat arrow function that has access to any props passed to the component. The ThemeProvider
component makes any variables defined in euiVars
and in customTheme
available to all its children, so it’s relatively easy to access.
We’ve done that here with props.theme.euiTitleColor
and props.theme.euiHeaderHeight
- to determine the font color of our h1
’s and calculate the minimum height of our StyledMain
.
Now we head back to our page and see how it looks. The biggest change is that the Inter
font is being applied to our h1
, and it’s size has been reset to 16px
. Don’t worry, that’s expected and is due to the css reset provided in the light theme css file from elastic-ui
. It’s also dodgerblue
. True commenting out our custom theme override and see how that changes.
To get properly sized fonts, we’ll need to wrap our h1
in an EuiText
component. It’s time to rev up our engines and dip into the component library.
import React from "react"
import { Layout } from "../../components"
import { EuiText } from "@elastic/eui"
export default function App() {
return (
<Layout>
<EuiText>
<h1>Phresh</h1>
</EuiText>
</Layout>
)
}
And just like that, we’re in business. There are quite a few elastic-ui
components to choose from, and their documentation is excellent. Feel free to play around with some of their examples for a minute, as we’ll take a deeper dive into them in a moment.
SIDENOTE: Throughout this tutorial series, there will be checkpoints where the frontend codebase will be made available through a codesandbox project. If you’re lost or unsure why your code isn’t working, simply click the link (like the one below) and play around with the files and file structure to catch up or correct your mistakes (or maybe mine!).
Check it out on Code Sandbox
phresh-frontend-part-1-project-setup
Building a Navbar
Every app needs a navbar and ours is no different. Since we’ll be adding many components to our project, it’s nice to start from the top. The navbar will be simple enough, but also provide us with a brief tour of how much elastic-ui
does for us.
Create a new component called Navbar.js
.
mkdir src/components/Navbar
touch src/components/Navbar/Navbar.js
And add this the file:
import React from "react"
import {
EuiIcon,
EuiHeader,
EuiHeaderSection,
EuiHeaderSectionItem,
EuiHeaderLink,
} from "@elastic/eui"
import styled from "styled-components"
const LogoSection = styled(EuiHeaderLink)`
padding: 0 2rem;
`
export default function Navbar({ ...props }) {
return (
<EuiHeader style={props.style || {}}>
<EuiHeaderSection>
<EuiHeaderSectionItem border="right">
<LogoSection href="/">
<EuiIcon type="cloudDrizzle" color="#1E90FF" size="l" /> Phresh
</LogoSection>
</EuiHeaderSectionItem>
</EuiHeader>
)
}
Cool. That’ll do. Before we go into this further, export it from the components/index.js
file and render it in the Layout.js
file above the StyledMain
component.
export { default as App } from "./App/App"
export { default as Layout } from "./Layout/Layout"
export { default as Navbar } from "./Navbar/Navbar"
And in the Layout.js
file:
import React from "react"
import { Helmet } from "react-helmet"
import { Navbar } from "../../components"
import styled, { ThemeProvider } from "styled-components"
import euiVars from "@elastic/eui/dist/eui_theme_light.json"
import "@elastic/edui/dist/eui_theme_light.css"
import "../../assets/css/fonts.css"
import "../../assets/css/override.css"
const customTheme = {
...euiVars,
euiTitleColor: "dodgerblue",
}
const StyledLayout = styled.div`
width: 100%;
max-width: 100vw;
min-height: 100vh;
background: rgb(224, 228, 234);
display: flex;
flex-direction: column;
`
const StyledMain = styled.main`
min-height: calc(100vh - ${(props) => props.theme.euiHeaderHeight} - 1px);
display: flex;
flex-direction: column;
& h1 {
color: ${(props) => props.theme.euiTitleColor};
}
`
export default function Layout({ children }) {
return (
<React.Fragment>
<Helmet>
<meta charSet="utf-8" />
<title>Phresh Cleaners</title>
<link rel="canonical" href="https://phreshcleaners.com" />
</Helmet>
<ThemeProvider theme={customTheme}>
<StyledLayout>
<Navbar />
<StyledMain>{props.children}</StyledMain>
</StyledLayout>
</ThemeProvider>
</React.Fragment>
)
}
You should now see the Navbar
displayed on the screen!
So what’s happening here? Let’s break it down.
It will probably help to look at the Header documentation as we go through this.
At the top of Navbar.js
, we’re importing a EuiHeader
component and 3 subcomponents that fulfill different roles in a header. We’re also adding additional styles to the EuiHeaderLink
component, by increasing the horizontal padding. This is how easy it is to add additional customization to what’s already provided by elastic-ui
. Instead of saying styled.div
or styled.section
, we wrap the component of our choosing with the styled()
function and pass in the appropriate css to the template string.
We’re also giving the EuiHeaderSectionItem
a border="right"
prop, but I’m not a fan of how it looks. After doing a little digging, I found an easy way to customize it to my liking. Head into the override.css
and file and add the following:
a:hover,
a:focus {
text-decoration: none !important;
color: dodgerblue;
}
.euiHeaderSectionItem:after {
top: 0;
}
Check it out on Code Sandbox
phresh-frontend-part-1-elastic-ui-navbar-intro
First, we’re setting a global style for all hovered and focused anchor tags to not show an underline and instead highlight dodgerblue.
Then we’re overriding the euiHeaderSectionItem:after
class to have top: 0
instead of the default top: 16px
. Feel free to skip that if you like the defaults, but I don’t.
Now we have something functional. Let’s fill out this `Navbar a bit with some extra goodies.
import React from "react"
import {
EuiIcon,
EuiHeader,
EuiHeaderSection,
EuiHeaderSectionItem,
EuiHeaderLinks,
EuiHeaderLink,
} from "@elastic/eui"
import styled from "styled-components"
const LogoSection = styled(EuiHeaderLink)`
padding: 0 2rem;
`
export default function Navbar({ ...props }) {
return (
<EuiHeader style={props.style || {}}>
<EuiHeaderSection>
<EuiHeaderSectionItem border="right">
<LogoSection href="/">
<EuiIcon type="cloudDrizzle" color="#1E90FF" size="l" /> Phresh
</LogoSection>
</EuiHeaderSectionItem>
<EuiHeaderSectionItem border="right">
<EuiHeaderLinks aria-label="app navigation links">
<EuiHeaderLink iconType="tear" href="#">
Find Cleaners
</EuiHeaderLink>
<EuiHeaderLink iconType="tag" href="#">
Find Jobs
</EuiHeaderLink>
<EuiHeaderLink iconType="help" href="#">
Help
</EuiHeaderLink>
</EuiHeaderLinks>
</EuiHeaderSectionItem>
</EuiHeaderSection>
</EuiHeader>
)
}
Nice. We’ve added another section of links directly next to the logo for finding cleaners, finding jobs, and getting help. Instead of importing the EuiIcon
component directly, we can pass an iconType
prop to the EuiHeaderLink
component and it’ll handle that for us. We do lose the ability to customize them however, so just be aware of that shortcut.
Ok now, let’s move on to the last part - the Avatar
.
By default, each EuiHeaderSection
is pushed as far away as possible due to the justify-content: space-between
used in it’s flex parent. So if we add another section, we can position our avatar in the far right of the Navbar
.
import React from "react"
import {
EuiAvatar,
EuiIcon,
EuiHeader,
EuiHeaderSection,
EuiHeaderSectionItem,
EuiHeaderSectionItemButton,
EuiHeaderLinks,
EuiHeaderLink,
} from "@elastic/eui"
import loginIcon from "../../assets/img/loginIcon.svg"
import styled from "styled-components"
const LogoSection = styled(EuiHeaderLink)`
padding: 0 2rem;
`
export default function Navbar({ user, ...props }) {
return (
<EuiHeader style={props.style || {}}>
<EuiHeaderSection>
<EuiHeaderSectionItem border="right">
<LogoSection href="/">
<EuiIcon type="cloudDrizzle" color="#1E90FF" size="l" /> Phresh
</LogoSection>
</EuiHeaderSectionItem>
<EuiHeaderSectionItem border="right">
<EuiHeaderLinks aria-label="app navigation links">
<EuiHeaderLink iconType="tear" href="#">
Find Cleaners
</EuiHeaderLink>
<EuiHeaderLink iconType="tag" href="#">
Find Jobs
</EuiHeaderLink>
<EuiHeaderLink iconType="help" href="#">
Help
</EuiHeaderLink>
</EuiHeaderLinks>
</EuiHeaderSectionItem>
</EuiHeaderSection>
<EuiHeaderSection>
<EuiHeaderSectionItemButton aria-label="User avatar">
{user?.profile ? (
<EuiAvatar size="l" name={user.profile.full_name} imageUrl={user.profile.image} />
) : (
<EuiAvatar size="l" color="#1E90FF" name="user" imageUrl={loginIcon} />
)}
</EuiHeaderSectionItemButton>
</EuiHeaderSection>
</EuiHeader>
)
}
We’ve made a few changes here. First, we’re importing a new file called loginIcon.svg
from our assets/img
directory. Feel free to use whatever icon you like. I simply copy pasted mine from tabler icons . Head to the website, type login
into search and paste the contents into a file called loginIcon.svg
. I also added a `transform=“scale(0.8) translate(5, 0)” property to the svg. The end result is a file like this:
<svg xmlns="http://www.w3.org/2000/svg" transform="scale(0.8) translate(5, 0)" width="44" height="44" viewBox="0 0 24 24" stroke-width="1" stroke="#FFFFFF" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2" />
<path d="M20 12h-13l3 -3m0 6l-3 -3" />
</svg>
If you don’t feel like going through all that trouble, just copy paste the code here into the appropriate file. You can also snag it from the codesandbox shown below.
We’re also telling our Navbar
to accept a user
prop and conditionally rendering the EuiAvatar
depending on whether or not the user is logged in. If they are and have a picture, the image will display in the avatar. If they’re logged in, but don’t have an image, the avatar will display the initials of their name or “anonymous”.
If they’re not logged in, like we see here, then we’ll get a login icon that we can use to authenticate the user.
Check it out on Code Sandbox
phresh-frontend-part-1-elastic-ui-navbar-filled-out
And there we have it! That’s enough for now.
On to the landing page hero.
Customize The Landing Page
Create a new component file called LandingPage
:
mkdir src/components/LandingPage
touch src/components/LandingPage/LandingPage.js
and add the following:
import React from "react"
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiFlexGroup,
EuiFlexItem,
} from "@elastic/eui"
import styled from "styled-components"
const StyledEuiPage = styled(EuiPage)`
flex: 1;
`
const StyledEuiPageContent = styled(EuiPageContent)`
border-radius: 50%;
`
export default function LandingPage(props) {
return (
<StyledEuiPage>
<EuiPageBody component="section">
<EuiFlexGroup>
<EuiFlexItem>
<StyledEuiPageContent horizontalPosition="center" verticalPosition="center">
</StyledEuiPageContent>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageBody>
</StyledEuiPage>
)
}
We’re using components taken directly from the elastic-ui
Page docs to structure our landing page, but the rest will be made from custom components. The EuiFlexGroup
and EuiFlexItem
components are simple wrappers around html elements styled with flexbox. We’ll occasionally use them for convenience and use custom flex components at other times.
Make sure we export it from our src/components/index.js
file
export { default as App } from "./App/App"
export { default as LandingPage } from "./LandingPage/LandingPage"
export { default as Layout } from "./Layout/Layout"
export { default as Navbar } from "./Navbar/Navbar"
and render it in our App.js
file instead of that placeholder h1
.
import React from "react"
import { LandingPage, Layout } from "../../components"
import { EuiText } from "@elastic/eui"
export default function App() {
return (
<Layout>
<LandingPage />
</Layout>
)
}
If we head to our browser, we should see a nice little circular panel right in the middle.
On top of that, we’ll include some nice svgs taken from illlustrations.co - a site from vijay verma hosting 100+ free illustrations. I went ahead and pulled about 8 of them for our landing page, customized the colors a bit, and added them to the codesandbox below. Stash those svgs in src/assets/img
.
Check it out on Code Sandbox
phresh-frontend-part-1-elastic-ui-landing-page-assets
Once you’ve got them stored, well add the first one to LandingPage.js
like so:
import React from "react"
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiFlexGroup,
EuiFlexItem,
} from "@elastic/eui"
import heroGirl from "../../assets/img/HeroGirl.svg"
import styled from "styled-components"
const StyledEuiPage = styled(EuiPage)`
flex: 1;
`
const StyledEuiPageContent = styled(EuiPageContent)`
border-radius: 50%;
`
const StyledEuiPageContentBody = styled(EuiPageContentBody)`
max-width: 400px;
max-height: 400px;
& > img {
width: 100%;
border-radius: 50%;
}
`
export default function LandingPage(props) {
return (
<StyledEuiPage>
<EuiPageBody component="section">
<EuiFlexGroup>
<EuiFlexItem grow={2}>
<StyledEuiPageContent horizontalPosition="center" verticalPosition="center">
<StyledEuiPageContentBody>
<img src={heroGirl} alt="girl" />
</StyledEuiPageContentBody>
</StyledEuiPageContent>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageBody>
</StyledEuiPage>
)
}
Looking good. Simple enough, but it could use a little extra umph..
Check it out on Code Sandbox
phresh-frontend-part-1-elastic-ui-landing-page-with-hero-svg
Now we’re going to add a Carousel
component that we’ll build from scratch to display these svgs.
Go ahead and make a Carousel.js
file.
mkdir src/components/Carousel
touch src/components/Carousel/Carousel.js
And in the component:
import React from "react"
import { EuiPanel } from "@elastic/eui"
import styled from "styled-components"
const CarouselWrapper = styled.div`
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`
const StyledEuiPanel = styled(EuiPanel)`
max-width: 450px;
max-height: 450px;
border-radius: 50%;
& > img {
width: 100%;
border-radius: 50%;
}
`
export default function Carousel({ items, interval = 3000, ...props }) {
const [current, setCurrent] = React.useState(0)
React.useEffect(() => {
const next = (current + 1) % items.length
const id = setTimeout(() => setCurrent(next), interval)
return () => clearTimeout(id)
}, [current, items.length, interval])
return (
<CarouselWrapper {...props}>
{items.map((item, i) =>
current === i ? (
<div key={i}>
<StyledEuiPanel paddingSize="l">{item.content}</StyledEuiPanel>
</div>
) : null
)}
</CarouselWrapper>
)
}
We start with a simple component that accepts a list of items and an interval.
At the top of our component we initialize state that holds the index of the current item being displayed, starting at 0``. Next, we use
React.useEffectto execute the item switching. The effect grabs whatever the next index is after current, and uses
setTimeoutto switch to the next item after a certain number of milliseconds have passed. The milliseconds are determined by the
intervalprop and default to
3000` (3 seconds).
Returning a function from React.useEffect
is an optional cleanup mechanism for effects. This cleanup function runs whenever the component unmounts, or whenever the items in the dependency array change - current
, items.length
, and interval
. This means that clearTimeout
is called before every new item change, ensuring that effects from the previous render are “cleaned up”.
This works, but it’s janky. Let’s see it in action.
First, export the Carousel.js
component and import it into the LandingPage.js
component.
export { default as App } from "./App/App"
export { default as Carousel } from "./Carousel/Carousel"
export { default as Layout } from "./Layout/Layout"
export { default as Navbar } from "./Navbar/Navbar"
Then, import each of the room-related images we grabbed from illustrations.co
. We’ll put them all in an array of items that we’ll pass to our Carousel
component.
import React from "react"
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiFlexGroup,
EuiFlexItem
} from "@elastic/eui"
import { Carousel } from "../../components"
import heroGirl from "../../assets/img/HeroGirl.svg"
import dorm from "../../assets/img/Bed.svg"
import bedroom from "../../assets/img/Bedroom.svg"
import bathroom from "../../assets/img/Bathroom.svg"
import livingRoom from "../../assets/img/Living_room_interior.svg"
import kitchen from "../../assets/img/Kitchen.svg"
import readingRoom from "../../assets/img/Reading_room.svg"
import tvRoom from "../../assets/img/TV_room.svg"
import styled from "styled-components"
const StyledEuiPage = styled(EuiPage)`
flex: 1;
`
const StyledEuiPageContent = styled(EuiPageContent)`
border-radius: 50%;
`
const StyledEuiPageContentBody = styled(EuiPageContentBody)`
max-width: 400px;
max-height: 400px;
& > img {
width: 100%;
border-radius: 50%;
}
`
const carouselItems = [
{ label: "dorm room", content: <img src={dorm} alt="bed" /> },
{ label: "bedroom", content: <img src={bedroom} alt="bedroom" /> },
{ label: "bathroom", content: <img src={bathroom} alt="bathroom" /> },
{ label: "living room", content: <img src={livingRoom} alt="living room" /> },
{ label: "kitchen", content: <img src={kitchen} alt="kitchen" /> },
{ label: "reading room", content: <img src={readingRoom} alt="reading room" /> },
{ label: "tv room", content: <img src={tvRoom} alt="tv room" /> }
]
export default function LandingPage(props) {
return (
<StyledEuiPage>
<EuiPageBody component="section">
<EuiFlexGroup direction="rowReverse">
<EuiFlexItem>
<Carousel items={carouselItems} />
</EuiFlexItem>
<EuiFlexItem>
<StyledEuiPageContent horizontalPosition="center" verticalPosition="center">
<StyledEuiPageContentBody>
<img src={heroGirl} alt="girl" />
</StyledEuiPageContentBody>
</StyledEuiPageContent>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageBody>
</StyledEuiPage>
)
}
Oh wow. That’s not something we want our users to see.
Check it out on Code Sandbox
phresh-frontend-part-1-elastic-ui-landing-page-carousel-basic
We should probably get more serious about our UI, and add some animations.
This is the perfect time to bring in framer-motion
. Framer motion describes themselves as a production-ready declarative animations library for React. I like the library because the API is exactly what I’d want a React animation library to look like. Also, the documentation is amazing and comes with a number of pre-built solutions.
Let’s dig in.
Animating Our Carousel
First, we import motion
and AnimatePresence
from framer-motion
. Then we wrap the section where we map over the carousel items with the AnimatePresence
component. This component helps animate items that enter and exit the DOM. We’re passing it the exitBeforeEnter
prop because we want each carousel item to animate out of the DOM before the next item appears.
Next, we convert the div
wrapping the StyledEuiPanel
into a motion.div
. Similar to styled components providing styled.div
, we get motion.div
from framer motion that works just like a normal div, but accepts additional props. We’ll pass it the name of the initial
state, the state name when it’s on the screen (animate
), and the exit
state name. We’ll also pass it a variants
prop that describes how the item should be animated at each stage. Finally, we’ll pass a transition
prop that describes how the transition should happen.
Here’s what it looks like:
import React from "react"
import { EuiPanel } from "@elastic/eui"
import { motion, AnimatePresence } from "framer-motion"
import styled from "styled-components"
const CarouselWrapper = styled.div`
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 450px;
min-width: 450px;
@media screen and (max-width: 450px) {
min-height: calc(100vw - 25px);
min-width: calc(100vw - 25px);
}
`
const StyledEuiPanel = styled(EuiPanel)`
height: 450px;
width: 450px;
max-width: 450px;
max-height: 450px;
border-radius: 50%;
& > img {
width: 100%;
border-radius: 50%;
}
@media screen and (max-width: 450px) {
height: calc(100vw - 25px);
width: calc(100vw - 25px);
}
`
const transitionDuration = 0.4
const transitionEase = [0.68, -0.55, 0.265, 1.55]
export default function Carousel({ items = [], interval = 3000, ...props }) {
const [current, setCurrent] = React.useState(0)
React.useEffect(() => {
const next = (current + 1) % items.length
const id = setTimeout(() => setCurrent(next), interval)
return () => clearTimeout(id)
}, [current, items.length, interval])
return (
<CarouselWrapper {...props}>
<AnimatePresence exitBeforeEnter>
{items.map((item, i) =>
current === i ? (
<React.Fragment key={i}>
<motion.div
key={i}
initial="left"
animate="present"
exit="right"
variants={{
left: { opacity: 0, x: -70 },
present: { opacity: 1, x: 0 },
right: { opacity: 0, x: 70 }
}}
transition={{ duration: transitionDuration, ease: transitionEase }}
>
<StyledEuiPanel paddingSize="l">{item.content}</StyledEuiPanel>
</motion.div>
</React.Fragment>
) : null
)}
</AnimatePresence>
</CarouselWrapper>
)
}
And look at that. When we head back to our Landing Page, we see a serious improvement.
Check it out on Code Sandbox
phresh-frontend-part-1-elastic-ui-landing-page-carousel-with-framer-motion
The images switch with a smooth animation, and our core Carousel
logic is still very simple.
In fact, it’s simple enough to extract into it’s own hook. Let’s do that now.
Create a new file in src/hooks
:
touch src/hooks/useCarousel.js
Then let’s add a new custom hook that we could use in any component.
import { useEffect, useRef, useState } from "react"
export function useCarousel(items, interval) {
const timeoutRef = useRef()
const [shouldAnimate, setShouldAnimate] = useState(true)
const [current, setCurrent] = useState(0)
useEffect(() => {
const next = (current + 1) % items.length
if (shouldAnimate) {
timeoutRef.current = setTimeout(() => setCurrent(next), interval)
}
return () => clearTimeout(timeoutRef.current)
}, [current, items.length, interval, shouldAnimate])
return { current, setShouldAnimate, timeoutRef }
}
Some slight modifications to our carousel logic. We use React.useRef
to give ourselves access to the timeout reference, and create state for shouldAnimate
to give ourselves the option to start/stop the animation if we need to. We also return an object with all of the appropriate variables, giving us access to any of the internals we might need.
Just to make sure it all works, let’s use it in our Carousel
component.
import React from "react"
import { EuiPanel } from "@elastic/eui"
import { motion, AnimatePresence } from "framer-motion"
import { useCarousel } from "../../hooks/useCarousel"
import styled from "styled-components"
const CarouselWrapper = styled.div`
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 450px;
min-width: 450px;
@media screen and (max-width: 450px) {
min-height: calc(100vw - 25px);
min-width: calc(100vw - 25px);
}
`
const StyledEuiPanel = styled(EuiPanel)`
height: 450px;
width: 450px;
max-width: 450px;
max-height: 450px;
border-radius: 50%;
& > img {
width: 100%;
border-radius: 50%;
}
@media screen and (max-width: 450px) {
height: calc(100vw - 25px);
width: calc(100vw - 25px);
}
`
const transitionDuration = 0.4
const transitionEase = [0.68, -0.55, 0.265, 1.55]
export default function Carousel({ items = [], interval = 3000, ...props }) {
const { current } = useCarousel(item, interval)
return (
<CarouselWrapper {...props}>
<AnimatePresence exitBeforeEnter>
{items.map((item, i) =>
current === i ? (
<React.Fragment key={i}>
<motion.div
key={i}
initial="left"
animate="present"
exit="right"
variants={{
left: { opacity: 0, x: -70 },
present: { opacity: 1, x: 0 },
right: { opacity: 0, x: 70 }
}}
transition={{ duration: transitionDuration, ease: transitionEase }}
>
<StyledEuiPanel paddingSize="l">{item.content}</StyledEuiPanel>
</motion.div>
</React.Fragment>
) : null
)}
</AnimatePresence>
</CarouselWrapper>
)
}
We only destructure current from the object returned by useCarousel
, since that’s all we need. And everything works just the same. Lovely.
Now why would we go through all that trouble?
Well, what if we wanted to add another component that shared the same logic but worked differently?
Creating an Animated Carousel Title
Let’s create a CarouselTitle
component to display the labels of each image.
mkdir src/components/CarouselTitle
touch src/components/CarouselTitle/CarouselTitle.js
and add the following to the file:
import React from "react"
import { EuiTitle } from "@elastic/eui"
import { motion, AnimatePresence } from "framer-motion"
import { useCarousel } from "../../hooks/useCarousel"
import styled from "styled-components"
const AnimatedTitle = styled.div`
margin-bottom: 1rem;
& h1 {
display: flex;
color: #212121;
margin: 0 0.25rem;
}
`
const TitleWrapper = styled.span`
display: flex;
flex-wrap: wrap;
`
const AnimatedCarouselTitle = styled.span`
position: relative;
display: flex;
justify-content: center;
width: 150px;
margin: 0 15px;
white-space: nowrap;
& .underline {
width: 170px;
height: 2px;
border-radius: 4px;
position: absolute;
bottom: -4px;
left: -10px;
background: black;
background: dodgerblue;
}
`
const transitionDuration = 0.4
const transitionEase = [0.68, -0.55, 0.265, 1.55]
const statement = `For busy people who need their`
export default function CarouselTitle({ items, interval = 3000 }) {
const { current } = useCarousel(items, interval)
return (
<AnimatedTitle>
<EuiTitle>
<TitleWrapper>
{statement.split(" ").map((word, i) => (
<h1 key={i}>{word}</h1>
))}
<AnimatePresence exitBeforeEnter>
<AnimatedCarouselTitle>
{items.map((item, i) => {
return (
current === i && (
<motion.span
key={i}
initial="top"
animate="present"
exit="bottom"
variants={{
top: { opacity: 0, y: -150 },
present: { opacity: 1, y: 0 },
bottom: { opacity: 0, y: 150 }
}}
transition={{ duration: transitionDuration, ease: transitionEase }}
>
{item.label}
</motion.span>
)
)
})}
<div className="underline" />
</AnimatedCarouselTitle>
</AnimatePresence>
<h1>cleaned.</h1>
</TitleWrapper>
</EuiTitle>
</AnimatedTitle>
)
}
This component looks very similar, with a few key differences. We’re animating from top down - a change of the y
position from -150
to 150
. We’re also animating words instead of images, so the layout and styles are changed considerably. We’re also adding an underline to the animated word, so that the animation appears to go down through the underline.
We’ll export this component and add it to our landing page as well.
export { default as App } from "./App/App"
export { default as Carousel } from "./Carousel/Carousel"
export { default as CarouselTitle } from "./CarouselTitle/CarouselTitle"
export { default as LandingPage } from "./LandingPage/LandingPage"
export { default as Layout } from "./Layout/Layout"
export { default as Navbar } from "./Navbar/Navbar"
Coupled with a simple title, we should have a cool carousel effect in two places on our LandingPage
when we bring our new component in.
import React from "react"
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiFlexGroup,
EuiFlexItem
} from "@elastic/eui"
import { Carousel, CarouselTitle } from "../../components"
import heroGirl from "../../assets/img/HeroGirl.svg"
import dorm from "../../assets/img/Bed.svg"
import bedroom from "../../assets/img/Bedroom.svg"
import bathroom from "../../assets/img/Bathroom.svg"
import livingRoom from "../../assets/img/Living_room_interior.svg"
import kitchen from "../../assets/img/Kitchen.svg"
import readingRoom from "../../assets/img/Reading_room.svg"
import tvRoom from "../../assets/img/TV_room.svg"
import styled from "styled-components"
const StyledEuiPage = styled(EuiPage)`
flex: 1;
padding-bottom: 5rem;
`
const LandingTitle = styled.h1`
font-size: 3.5rem;
margin: 2rem 0;
`
const StyledEuiPageContent = styled(EuiPageContent)`
border-radius: 50%;
`
const StyledEuiPageContentBody = styled(EuiPageContentBody)`
max-width: 400px;
max-height: 400px;
& > img {
max-width: 100%;
border-radius: 50%;
object-fit: cover;
}
`
const carouselItems = [
{ label: "dorm room", content: <img src={dorm} alt="dorm room" /> },
{ label: "bedroom", content: <img src={bedroom} alt="bedroom" /> },
{ label: "bathroom", content: <img src={bathroom} alt="bathroom" /> },
{ label: "living room", content: <img src={livingRoom} alt="living room" /> },
{ label: "kitchen", content: <img src={kitchen} alt="kitchen" /> },
{ label: "reading room", content: <img src={readingRoom} alt="reading room" /> },
{ label: "tv room", content: <img src={tvRoom} alt="tv room" /> }
]
export default function LandingPage() {
return (
<StyledEuiPage>
<EuiPageBody component="section">
<EuiFlexGroup direction="column" alignItems="center">
<EuiFlexItem>
<LandingTitle>Phresh Cleaners</LandingTitle>
</EuiFlexItem>
<EuiFlexItem>
<CarouselTitle items={carouselItems} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="rowReverse">
<EuiFlexItem>
<Carousel items={carouselItems}/>
</EuiFlexItem>
<EuiFlexItem>
<StyledEuiPageContent horizontalPosition="center" verticalPosition="center">
<StyledEuiPageContentBody>
<img src={heroGirl} alt="girl" />
</StyledEuiPageContentBody>
</StyledEuiPageContent>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageBody>
</StyledEuiPage>
)
}
Ok, we’re in business.
But something doesn’t feel right.
These two carousel items aren’t synced up - they’re technically operating independently. Let’s change up our structure a bit and control these two components from the LandingPage
.
Go ahead and import the useCarousel
hook and initialize it in our LandingPage
component. Then, pass the current variable to each of our carousel components.
import React from "react"
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiFlexGroup,
EuiFlexItem
} from "@elastic/eui"
import { Carousel, CarouselTitle } from "../../components"
import { useCarousel } from "../../hooks/useCarousel"
import heroGirl from "../../assets/img/HeroGirl.svg"
import dorm from "../../assets/img/Bed.svg"
import bedroom from "../../assets/img/Bedroom.svg"
import bathroom from "../../assets/img/Bathroom.svg"
import livingRoom from "../../assets/img/Living_room_interior.svg"
import kitchen from "../../assets/img/Kitchen.svg"
import readingRoom from "../../assets/img/Reading_room.svg"
import tvRoom from "../../assets/img/TV_room.svg"
import styled from "styled-components"
const StyledEuiPage = styled(EuiPage)`
flex: 1;
padding-bottom: 5rem;
`
const LandingTitle = styled.h1`
font-size: 3.5rem;
margin: 2rem 0;
`
const StyledEuiPageContent = styled(EuiPageContent)`
border-radius: 50%;
`
const StyledEuiPageContentBody = styled(EuiPageContentBody)`
max-width: 400px;
max-height: 400px;
& > img {
max-width: 100%;
border-radius: 50%;
object-fit: cover;
}
`
const carouselItems = [
{ label: "dorm room", content: <img src={dorm} alt="dorm room" /> },
{ label: "bedroom", content: <img src={bedroom} alt="bedroom" /> },
{ label: "bathroom", content: <img src={bathroom} alt="bathroom" /> },
{ label: "living room", content: <img src={livingRoom} alt="living room" /> },
{ label: "kitchen", content: <img src={kitchen} alt="kitchen" /> },
{ label: "reading room", content: <img src={readingRoom} alt="reading room" /> },
{ label: "tv room", content: <img src={tvRoom} alt="tv room" /> }
]
export default function LandingPage() {
const { current } = useCarousel(carouselItems, 3000)
return (
<StyledEuiPage>
<EuiPageBody component="section">
<EuiFlexGroup direction="column" alignItems="center">
<EuiFlexItem>
<LandingTitle>Phresh Cleaners</LandingTitle>
</EuiFlexItem>
<EuiFlexItem>
<CarouselTitle items={carouselItems} current={current} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="rowReverse">
<EuiFlexItem>
<Carousel items={carouselItems} current={current} />
</EuiFlexItem>
<EuiFlexItem>
<StyledEuiPageContent horizontalPosition="center" verticalPosition="center">
<StyledEuiPageContentBody>
<img src={heroGirl} alt="girl" />
</StyledEuiPageContentBody>
</StyledEuiPageContent>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageBody>
</StyledEuiPage>
)
}
Now, we’ll update each component to accept a current prop instead of creating the variable from the useCarousel
hook.
import React from "react"
import { EuiPanel } from "@elastic/eui"
import { motion, AnimatePresence } from "framer-motion"
import styled from "styled-components"
const CarouselWrapper = styled.div`
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 450px;
min-width: 450px;
@media screen and (max-width: 450px) {
min-height: calc(100vw - 25px);
min-width: calc(100vw - 25px);
}
`
const StyledEuiPanel = styled(EuiPanel)`
height: 450px;
width: 450px;
max-width: 450px;
max-height: 450px;
border-radius: 50%;
& > img {
width: 100%;
border-radius: 50%;
}
@media screen and (max-width: 450px) {
height: calc(100vw - 25px);
width: calc(100vw - 25px);
}
`
const transitionDuration = 0.3
const transitionEase = [0.68, -0.55, 0.265, 1.55]
export default function Carousel({ items = [], current }) {
return (
<CarouselWrapper>
<AnimatePresence exitBeforeEnter>
{items.map((item, i) =>
current === i ? (
<React.Fragment key={i}>
<motion.div
key={index}
initial="left"
animate="present"
exit="right"
variants={{
left: { opacity: 0, x: -70 },
present: { opacity: 1, x: 0 },
right: { opacity: 0, x: 70 },
}}
transition={{ duration: transitionDuration, ease: transitionEase }}
>
<StyledEuiPanel paddingSize="l">{item.content}</StyledEuiPanel>
</motion.div>
</React.Fragment>
) : null
)}
</AnimatePresence>
</CarouselWrapper>
)
}
And in our CarouselTitle
component:
import React from "react"
import { EuiTitle } from "@elastic/eui"
import { motion, AnimatePresence } from "framer-motion"
import styled from "styled-components"
const AnimatedTitle = styled.div`
margin-bottom: 1rem;
& h1 {
display: flex;
color: #212121;
margin: 0 0.25rem;
}
`
const TitleWrapper = styled.span`
display: flex;
flex-wrap: wrap;
`
const AnimatedCarouselTitle = styled.span`
position: relative;
display: flex;
justify-content: center;
width: 150px;
margin: 0 15px;
white-space: nowrap;
& .underline {
width: 170px;
height: 2px;
border-radius: 4px;
position: absolute;
bottom: -4px;
left: -10px;
background: black;
background: dodgerblue;
}
`
const transitionDuration = 0.4
const transitionEase = [0.68, -0.55, 0.265, 1.55]
const statement = `For busy people who need their`
export default function CarouselTitle({ items, current }) {
return (
<AnimatedTitle>
<EuiTitle>
<TitleWrapper>
{statement.split(" ").map((word, i) => (
<h1 key={i}>{word}</h1>
))}
<AnimatePresence exitBeforeEnter>
<AnimatedCarouselTitle>
{items.map((item, i) => {
return (
current === i && (
<React.Fragment key={i}>
<motion.span
key={i}
initial="top"
animate="present"
exit="bottom"
variants={{
top: { opacity: 0, y: -150 },
present: { opacity: 1, y: 0 },
bottom: { opacity: 0, y: 150 },
}}
transition={{ duration: transitionDuration, ease: transitionEase }}
>
{item.label}
</motion.span>
</React.Fragment>
)
)
})}
<div className="underline" />
</AnimatedCarouselTitle>
</AnimatePresence>
<h1>cleaned.</h1>
</TitleWrapper>
</EuiTitle>
</AnimatedTitle>
)
}
And just like that, we’ve built ourselves a sweet looking landing page with a customized carousel element.
Check it out on Code Sandbox
phresh-frontend-part-1-elastic-ui-landing-page-carousel-title-with-framer-motion
We’re ready for the next stage.
Wrapping Up and Resources
We’ve done quite a bit here to get our frontend up and running. We bootstrapped a react application, brought in elastic-ui
components, and styled them with styled-components
. We built a couple custom Carousel
components, extracted the core logic into our own useCarousel
hook, and animated them with framer-motion
.
That’s more than enough for one day, so we’ll call it quits for now.
In the next post, we’ll add routing to our app with react-router
and build out login, sign up and profile pages. Afterwards, we’ll implement authentication on the front end using redux
and axios
.
If you missed any of the links or documentation, here’s a good place to catch up:
- React Hooks docs
- React Helmet repo
- Elastic UI docs
- Elastic UI Header docs
- Elastic UI Page docs
- Elastic UI Panel docs
- Elastic UI Flex docs
- Styled Components docs
- Framer Motion docs
- Illlustrations.co homepage
- Tabler Icons homepage
Evaluations and SQL Aggregations in FastAPI
Frontend Navigation with React Router