Demo App : modokemdev.com/speakers-app
This repository was built following the Designing React Components PluralSight course by Peter Kellner (Here is the course repository in GitHub). It is a Next.js app with Tailwind CSS style. Personally, I don't like the project setup. You can find an updated setup in my speakers-app repository which I used to deploy to GitHub Pages. One of the things I liked was the implementation of the json-server package that allows to easily setup a fake REST API for axios calls.
The next-tailwind-app can run locally on your machine. Clone the repository and run npm run dev
.
Here are the notes I took from the course:
npm init -y
npm install react react-dom next --save
package.json
:"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
Next.js has file-based routing, meaning that any component in the pages directory gets a route.
pages/index.js
:function Page() {
return (
<div>
<h1>Hello From Pluralsight</h1>
</div>
)
}
export default Page
npm run dev
Next.js looks for static files in the public folder. For example, we could have an image at public/images/header.png
and it would be served at http://localhost:3000/images/header.png
.
pages/speakers.js
page:function Page() {
return (
<div>
<img src="images/header.png" />
<img src="images/menu.gif" />
<img src="images/searchbar.gif" />
<img src="images/speakers.png" />
<img src="images/footer.png" />
</div>
)
}
export default Page
npm run dev
and go to http://localhost:3000/speakers
To split the components, we need to create a src/components
folder and add all the components.
pages/speakers.js
:import Menu from '../src/components/Menu/Menu'
import Header from '../src/components/Header/Header'
import SpeakerSearchBar from '../src/components/SpeakerSearchBar/SpeakerSearchBar'
import Speakers from '../src/components/Speakers/Speakers'
import Footer from '../src/components/Footer/Footer'
export default function Page() {
return (
<div>
<Header />
<Menu />
<SpeakerSearchBar />
<Speakers />
<Footer />
</div>
)
}
We can divide the Speakers
component into an array of 3 images.
components/Speakers/Speakers.js
:import React from 'react'
const Speakers = () => {
const speakers = [
{
imageSrc: 'speaker-component-1124',
name: 'Douglas Crockford',
},
{
imageSrc: 'speaker-component-1530',
name: 'Tamara Baker',
},
{
imageSrc: 'speaker-component-10803',
name: 'Eugene Chuvyrov',
},
]
return (
<div>
{speakers.map(({ imageSrc, name }) => {
return (
<img src={`/images/${imageSrc}.png`} alt={name} key={imageSrc}></img>
)
})}
</div>
)
}
export default Speakers
An HOC component is a function that takes a component and returns a new component.
const EnhancedComponent = higherOrderComponent(WrappedComponent);
Speakers/Speaker.js
to add a Speakers/withData.js
HOC:import React from 'react'
import withData from './withData'
const Speakers = ({ speakers }) => {
return (
<div>
{speakers.map(({ imageSrc, name }) => {
return (
<img src={`/images/${imageSrc}.png`} alt={name} key={imageSrc}></img>
)
})}
</div>
)
}
const maxSpeakersToShow = 2
export default withData(maxSpeakersToShow)(Speakers)
Speakers/withData.js
:const withData = (maxSpeakersToShow) => (Component) => {
const speakers = [
{ imageSrc: 'speaker-component-1124', name: 'Douglas Crockford' },
{ imageSrc: 'speaker-component-1530', name: 'Tamara Baker' },
{ imageSrc: 'speaker-component-10803', name: 'Eugene Chuvyrov' },
]
return () => {
const limitSpeakers = speakers.slice(0, maxSpeakersToShow)
return <Component speakers={limitSpeakers}></Component>
}
}
export default withData
A component with a render prop takes a function that returns a React element and calls it instead of implementing its own render logic.
The React Context API is designed to share data that can be considered global to all component descendants in component tree.
Pre-built themes | Low level of primitive based |
---|---|
Bootstrap | Tailwind CSS |
Foundation | Tachyons |
Bulma | |
Materialize CSS |
<button class="btn btn-primary">Bootstrap Button</button>
Pros | Cons |
---|---|
Start theming your app immediately | Bootstrap may not meet all your needs |
No CSS work needed | You must extend Bootstrap |
Consistent look and feel | CSS or SASS expertise required |
<button
class="bg-blue-400 hover:bg-blue-600 text-white font-bold px-2 px-4 rounded"
>
Tailwind CSS Button
</button>
Pros | Cons |
---|---|
The look you want with responsiveness on all platforms | Upfront investment required at the start |
Customizations are straightforward | You build your styles and classes from primitives |
More control using Tailwind CSS
Easy to customize
To install Tailwind, you can use the official documentation. You can also take a loot at the official GitHub examples. Here is the official example for Next.js.
Since Tailwind is purely about using predefined classes nothing has to change in our built process that affects our production build. That is, in production, our app simply needs to reference or import the Tailwind CSS created file. However, it's likely you will want to make customizations to Tailwind. To do this you want to include new devDependencies in your package.json
file.
npm install @fullhuman/postcss-purgecss postcss-preset-env tailwindcss --save-dev
postcss.config.js
file and adding the following configuration:module.exports = {
plugins: [
'tailwindcss',
process.env.NODE_ENV === 'production'
? [
'@fullhuman/postcss-purgecss',
{
content: [
'./pages/**/*.{js,jsx,ts,tsx}',
'./src/**/*.{js,jsx,ts,tsx}',
],
defaultExtractor: (content) =>
content.match(/[\w-/:]+(?<!:)/g) || [],
},
]
: undefined,
'postcss-preset-env',
].filter(Boolean),
}
Next, create a CSS file for your Tailwind styles. We've used styles/index.css
for this example:
@tailwind base;
@tailwind components;
@tailwind utilities;
_app.js
component to make them available globally:import '../styles/index.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
npm install
to ensure everything is updated.tailwind.js
page with an example templatenpm run dev
and navigate to http://localhost:3000/tailwind
Add a button.cs
file inside the styles
folder. Instead of adding CSS attributes, we use the tailwind @apply
directive and follow that by the classes we want to combine into a new CSS class name. In our case this new CSS class name is btn-blue
.btn-blue {
@apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}
Finally, add the @import './button.css';
directive to the index.css
file. You can now use the new button as follow:
<button className="btn-blue">Subscribe</button>
Different pages on site | Common components |
---|---|
Home | Header |
Speakers | Menu |
Sessions | Footer |
Schedules |
src/components/Layout/Layout.js
component:import React from 'react'
import Header from '../Header/Header'
import Menu from '../Menu/Menu'
import Footer from '../Footer/Footer'
const Layout = ({ children }) => (
<div className="mx-4 my-3">
<Header />
<Menu />
{children}
<Footer />
</div>
)
export default Layout
Layout
component.Header/Header.js
, Menu/Menu.js
and Footer/Footer.js
to include Tailwind CSS.Speakers/Speakers.js
to display more content and the filter bar all styled with Tailwind CSS.Speakers
component into multiple smaller components: SpeakerSearchBar
and Speaker
.Speaker
component by adding SpeakerFavoriteButton
and SpeakerImage
components.SpeakerImage
component. There could be over 100 images to load. We only want to render the image that is shown in the browser. To do this, we add the following package:npm install react-simple-img --save
let state = {
searchQuery: 'Crockford',
}
Speakers.js
, create a new state searchQuery
using the React useState
Hook.searchQuery
to the SpeakerSearchBar
component.SpeakerFavoriteButton
and Speaker
components with onFavoriteToggle
handler property.onFavoriteToggleHandler
function in Speakers
component to update the isFavorite
status.state: {
speakers: [{speaker1},{speaker2}, ...]
}
setState: ({
speakers: [{speaker1},{speaker2withNewValue}, ...]
});
useState
hook and pass the initial value of the speakersArray
:const [speakers, setSpeakers] = useState(speakersArray)
setSpeakers
with the new array and the state is updated.Representational state transfer (REST) is a software architectural style that defines a set of constraints to be used for creating Web services.
HTTP VERB | URL Endpoint |
---|---|
GET | http://localhost:4000/speakers |
PUT | http://loclalhost:4000/speakers/$ID |
axios
for get
and put
calls :npm install axios --save
axios
to the Speakers
component :import axios from 'axios'
speakersArray
.useState
that initializes our speakers to an empty array: useState([])
.Speakers
component loads, use the react hook useEffect
, which is design to add side effects to our functional component. The side effect we want is to add speakers to the speaker state.import React, { useState, useEffect } from 'react'
useEffect(() => {
const fetchData = async () => {
const response = await axios.get('http://localhost:4000/speakers')
setSpeakers(response.data)
}
fetchData()
}, [])
onFavoriteToggleHandler
function to use axios
:async function onFavoriteToggleHandler(speakerRec) {
await axios.put(
`http://localhost:4000/speakers/${speakerRec.id}`,
toggledSpeakerRec
)
}
json-server
:npm install json-server --save-dev
package.json
:"scripts": {
"json-server": "json-server --watch db.json --port 4000 --delay 500"
}
db.json
file in the root of the project.npm run dev
and npm run json-server
const response = await axios.get('http://..')
Loading...
setSpeakers(response.data)
Create constants for the different status you want to track:
const REQUEST_STATUS = {
LOADING: 'loading',
SUCCESS: 'success',
ERROR: 'error',
}
Use web hooks and try/catch elements to manage the different status.
We have 3 different state primitives to manage the speakers list:
const [speakers, setSpeakers] = useState([])
const [status, setStatus] = useState(REQUEST_STATUS.LOADING)
const [error, setError] = useState({})
We can consolidate the state management of these 3 primitive state values into a reducer.
A Reducer is a function that takes in an old state, along with an object called action, and returns a new state.
;(previousState, action) => newState
Extract the actions into src/actions/request.js
and the reducers into src/reducers/request.js
.
... unfinished