Welcome to the new serlo.org frontend.
Install Node.js (>=10) and yarn on your system.
Clone this repo, install dependencies and start the dev server:
git clone https://github.com/serlo/frontend.git
cd frontend
yarn
yarn dev
The server is now running on localhost:3000
.
Routes are mapped to individual files in the pages
-folder. Create a page by adding following file:
// pages/helloworld.tsx
function HelloWorld() {
return <p>Welcome to the frontend!</p>
}
export default HelloWorld
Visit localhost:3000/helloworld
to view this page.
You can attach styles to html elements and use them in your component:
// pages/helloworld.tsx
import styled from 'styled-components'
function HelloWorld() {
return <BigParagraph>Welcome to the frontend!</BigParagraph>
}
const BigParagraph = styled.p`
text-align: center;
font-size: 3rem;
color: lightgreen;
`
export default HelloWorld
Use functional components and hooks to split your code into reusable pieces. Some basic features are shown in this example:
// pages/helloworld.tsx
import React from 'react'
import styled from 'styled-components'
function HelloWorld() {
return <ClickMeTitle title="Welcome to the frontend!" />
}
function ClickMeTitle(props) {
const { title } = props
const [clicked, setClicked] = React.useState(false)
const smiley = clicked ? ' :)' : ''
return (
<BigParagraph onClick={() => setClicked(!clicked)}>
{title + smiley}
</BigParagraph>
)
}
const BigParagraph = styled.p`
text-align: center;
font-size: 3rem;
color: lightgreen;
`
export default HelloWorld
Visit localhost:3000/helloworld
. Click on the text. Every click should toggle a smiley face:
We love types. They help us to maintain code and keep the codebase consistent. We also love rapid development and prototyping. You decide: Add your type declarations immediately as you code or later when the codebase stabilizes. The choice is up to you:
function HelloWorld() {
return <Greeter title="Hello" subline="Welcome to the frontend!" />
}
interface GreeterProps {
title: string
subline?: string
}
function Greeter({ title, subline }: GreeterProps) {
return (
<>
<h1>{title}</h1>
{subline && <small>{subline}</small>}
</>
)
}
export default HelloWorld
The frontend is a growing collection of components. Package every part of the UI as a component, save them in src/components
and let the file name match the components name. Export the component as a default and type the props. A complete component file would look like this:
// src/components/Greeter.tsx
interface GreeterProps {
title: string
subline?: string
}
export default function Greeter({ title, subline }: GreeterProps) {
return (
<>
<h1>{title}</h1>
{subline && <small>{subline}</small>}
</>
)
}
Visit localhost:3000/__gallery
for a quick overview of all components. Consider adding your new component into the gallery.
Users will come to the frontend using very different devices, from narrow smartphones to very wide screens. Adapt your components and change there appearing with media queries:
import styled from 'styled-components'
function HelloWorld() {
return (
<ResponsiveBox>
<GrowingParagraph>Hallo</GrowingParagraph>
<GrowingParagraph>Welt</GrowingParagraph>
</ResponsiveBox>
)
}
const ResponsiveBox = styled.div`
display: flex;
@media (max-width: 500px) {
flex-direction: column;
}
`
const GrowingParagraph = styled.p`
flex-grow: 1;
text-align: center;
font-size: 2rem;
padding: 16px;
background-color: lightgreen;
`
export default HelloWorld
This example makes use of flexbox. On wide screens, both paragraphs are shown next to each other:
On smaller screens, they are below each other:
We can improve the previous example by extracting commenly used constants like breakpoints or colors into a theme. The file src/theme.tsx
defines our global theme which you can access in every component:
import styled from 'styled-components'
function HelloWorld() {
return (
<ResponsiveBox>
<GrowingParagraph>Hallo</GrowingParagraph>
<GrowingParagraph>Welt</GrowingParagraph>
</ResponsiveBox>
)
}
const ResponsiveBox = styled.div`
display: flex;
@media (max-width: ${props => props.theme.breakpoints.sm}) {
flex-direction: column;
}
`
const GrowingParagraph = styled.p`
flex-grow: 1;
text-align: center;
font-size: 2rem;
padding: 16px;
background-color: ${props => props.theme.colors.brand};
`
export default HelloWorld
Visit localhost:3000/_colors
to see all the colors predefined in the theme.
There exists a bunch of different length units. Most of the time, px is fine. Sometimes there are better alternativs, especially in regard of a11y:
- Use
rem
forfont-size
, so users can zoom the text (e.g. farsighted people or users on 4k monitors) - Use dimensionless values for
line-height
to scale well. - Test your component how it behaves when text zooms and eventually make adjustments.
Add some eye candy by using icons. We integrated Font Awesome and adding icons is straight forward:
import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCoffee } from '@fortawesome/free-solid-svg-icons'
function HelloWorld() {
return (
<BigIcon>
<FontAwesomeIcon icon={faCoffee} size="1x" />
</BigIcon>
)
}
const BigIcon = styled.div`
text-align: center;
font-size: 3rem;
color: brown;
margin: 30px;
`
export default HelloWorld
Often you need two components with only slightly different styles. Adapt your styles based on props:
import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCandyCane } from '@fortawesome/free-solid-svg-icons'
function HelloWorld() {
return (
<BigIcon iconColor="pink">
<FontAwesomeIcon icon={faCandyCane} size="1x" />
</BigIcon>
)
}
const BigIcon = styled.div<{ iconColor: string }>`
text-align: center;
font-size: 3rem;
color: ${props => props.iconColor};
margin: 30px;
`
export default HelloWorld
This is one of the rare places where types are mandatory.
To boost your creativity, we included a bunch of useful css helper from polished:
import React from 'react'
import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCandyCane } from '@fortawesome/free-solid-svg-icons'
import { lighten } from 'polished'
function HelloWorld() {
const [lighter, setLighter] = React.useState(0)
return (
<>
<p>Click it:</p>
<BigIcon lighter={lighter} onClick={() => setLighter(lighter + 0.01)}>
<FontAwesomeIcon icon={faCandyCane} size="1x" />
</BigIcon>
</>
)
}
const BigIcon = styled.div<{ lighter: number }>`
text-align: center;
font-size: 3rem;
color: ${props => lighten(props.lighter, 'pink')};
margin: 30px;
`
export default HelloWorld
Import your helper from polished and use it in interpolations.
Put static content like images or documents into the public/_assets
folder.
Example: The file public/_assets/img/placeholder.png
is accessible via localhost:3000/_assets/img/placeholder.png
You can use assets in your components:
function HelloWorld() {
return <img src="/_assets/img/placeholder.png" alt="placeholder" />
}
export default HelloWorld
You can import a svg directly. They are inlined and usable as component:
import SerloLogo from '../public/_assets/img/serlo-logo.svg'
function HelloWorld() {
return <SerloLogo />
}
export default HelloWorld
Format your code in a consistent way by running
yarn prettify
Make sure your code is properly formatted before every commit.
You can add elements that pop out of the page with Tippy. A basic drop button looks like this:
import styled from 'styled-components'
import Tippy from '@tippyjs/react'
function HelloWorld() {
return (
<Wall>
<Tippy
content={<Drop>Surprise )(</Drop>}
trigger="click"
placement="bottom-start"
>
<button>Click Me!</button>
</Tippy>
</Wall>
)
}
const Wall = styled.div`
margin-top: 100px;
display: flex;
justify-content: center;
`
const Drop = styled.div`
background-color: lightgreen;
padding: 5px;
box-shadow: 8px 8px 2px 1px rgba(0, 255, 0, 0.2);
`
export default HelloWorld
Surround the target element with the Tippy
component and pass the content to it. There are many more props to explore.
Show information to the user with modals. react-modal provides the necessary functionality. This example shows how you can get started:
import React from 'react'
import Modal from '../components/Modal' // our wrapper
const centeredModal = {
overlay: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
content: {
position: 'static'
}
}
function HelloWorld() {
const [open, setOpen] = React.useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Open modal</button>
<Modal
isOpen={open}
onRequestClose={() => setOpen(false)}
style={centeredModal}
>
This is the content of the modal
</Modal>
</>
)
}
export default HelloWorld
You handle the state by yourself. The Modal
component has many options available. Import the modal from src/reactmodal.tsx
. This takes care of the app element.
You can use KaTeX to render formulas:
import styled from 'styled-components'
import Math from '../src/math'
function HelloWorld() {
return (
<>
<Paragraph>
This changed the world:{' '}
<Math formula={'c = \\pm\\sqrt{a^2 + b^2}'} inline />.
</Paragraph>
<Paragraph>This too:</Paragraph>
<CenteredParagraph>
<Math formula={'E = mc^2'} />
</CenteredParagraph>
</>
)
}
const Paragraph = styled.p`
margin: 20px;
font-size: 18px;
`
const CenteredParagraph = styled.p`
text-align: center;
font-size: 18px;
`
export default HelloWorld
Our math component takes two props: formula
is the LaTeX string, inline
is optional and will make the formula a bit smaller. The rendered formula is a span
that can be placed anywhere.
Data fetching is handled by our GraphQL data fetcher. Look at src/fetcher/README.md
for a detailed explanation.
Build and run the frontend with these commands:
yarn start
This will trigger a production build (docker
and docker-compose
need to be installed). To stop the created docker image, run:
yarn stop
To get detailed information about bundle size and a summarize of all output artifacts, run this:
yarn analyze
Results are saved to .next/analyze/client.html
and .next/analyze/server.html
.
If some part of a page is heavy and only relevant for a smaller fraction of users, import it dynamically. Write your component as usual:
// src/fancycomponent.tsx
function FancyComponent() {
return <p>This is some heavy component</p>
}
export default FancyComponent
Use a dynamic import to load the component:
// pages/helloworld.tsx
import React from 'react'
import dynamic from 'next/dynamic'
const FancyComponent = dynamic(() => import('../src/fancycomponent'))
function HelloWorld() {
const [visible, setVisible] = React.useState(false)
return (
<>
<p>
<button onClick={() => setVisible(true)}>Load ...</button>
</p>
{visible && <FancyComponent />}
</>
)
}
export default HelloWorld
The source code of FancyComponent
is splitting into a separate chunk and is only loaded when users click the button.
You can extend components by adding style snippets. These snippets are functions that add new props to a styled component:
import styled from 'styled-components'
function HelloWorld() {
return (
<>
<ChatParagraph side="left">Hey, how are you?</ChatParagraph>
<ChatParagraph side="right">I'm fine!</ChatParagraph>
</>
)
}
interface SideProps {
side: 'left' | 'right'
}
function withSide(props: SideProps) {
if (props.side === 'left') {
return `
color: blue;
text-align: left;
`
} else if (props.side === 'right') {
return `
color: green;
text-align: right;
`
} else {
return ''
}
}
const ChatParagraph = styled.p<SideProps>`
${withSide}
margin: 20px;
`
export default HelloWorld
This example adds the side
prop to the ChatParagraph
and allows users to change the appearance of the component.
You can reuse this function in another component:
const AnotherChatParagraph = styled.p<SideProps>`
${withSide}
margin: 15px;
border: 3px solid gray;
`
Your pages get wrapped in two components, _document.js and _app.js. You can override both files. The document contains everything that is outside of your react app, especially the html and body tag. This is a good place to set styles on these or to define the language. The document is rendered on the server only.
The app is the entrypoint of your page and is rendered client-side as well. You can add global providers or import css files here.
It is possible to listen to scroll and resize events as a very very (!!) last resort for responsive design, e.g. if media queries are insufficient. Use useEffect
to accomplish this task:
import React from 'react'
import styled from 'styled-components'
function HelloWorld() {
const [gray, setGray] = React.useState(false)
React.useEffect(() => {
function handleScroll() {
const scrollY = window.pageYOffset
setGray(scrollY > 250)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
return (
<BigDiv>
<Par gray={gray}>Please scroll down a little bit ...</Par>
</BigDiv>
)
}
const BigDiv = styled.div`
height: 4000px;
`
const Par = styled.p<{ gray: boolean }>`
font-size: 3rem;
text-align: center;
margin-top: 500px;
${props => (props.gray ? 'color:lightgray;' : '')}
`
export default HelloWorld
This text will gray out if you scroll down. useEffect
with an empty dependency array is called once on mount. The return value is called when the component unmounts and will remove the event listener. Set the state directly within the event handler.
Here is a list of included peer dependencies:
styled-components
depends onreact-is
No, we are not using any css resets. Each component should reset their own styles.
No, styled components takes care of this already.
Only if it is absolutely necessary. You are able to import external .css
files in pages/_app.js
. These stylesheets are always global and included in every page. If possible, use a package that supports styled components.
Some client specific objects (window, document) are causing trouble with server side rendering. What can I do?
Delay these parts of the code after your component mounted, using the useEffect
hook:
import React from 'react'
import styled from 'styled-components'
function HelloWorld() {
const [href, setHref] = React.useState(undefined)
React.useEffect(() => {
setHref(window.location.href)
}, [])
return href ? <BigDiv>Your site's url is {href}</BigDiv> : null
}
const BigDiv = styled.div`
text-align: center;
margin-top: 100px;
`
export default HelloWorld
Using the state is important: This ensures that server side rendering and client side hydration matches up.
The most idomatic way to do this is checking the type of window:
if (typeof window === 'undefined') {
// serverside
}
A bigger example:
function HelloWorld(props) {
return <>{JSON.stringify(props.data)}</>
}
HelloWorld.getInitialProps = async () => {
if (typeof window === 'undefined') {
const fs = await import('fs')
const util = await import('util')
const data = await util.promisify(fs.readFile)('package.json', 'utf-8')
console.log(data)
return { data: JSON.parse(data) }
}
return {}
}
export default HelloWorld
The fs
module is only available in nodejs, but it's ok to use it when you check that you are serverside and load it with a dynamic import. There is also some async/await syntax shown here.
To focus a html element, you need access to the underlying DOM node. Use the ref hook for this.
JavaScript compilers allow a greater range of syntax. Here is a small cheatsheet.
const { title, url } = props
// -->
const title = props.title
const url = props.url
const [open, setOpen] = React.useState(false)
// -->
const __temp = React.useState(false)
const open = __temp[0]
const setOpen = __temp[1]
return { title, content }
// -->
return { title: title, content: content }
return `The key ${key} can not be found in ${db}.`
// -->
return 'The key ' + key + ' can not be found in ' + db + '.'
return <Par gray={true}>This is a paragraph</Par>
// -->
return React.createElement(Par, { gray: true }, `This is a paragraph`)
Generally, you can't and shouldn't. Extract the state to the parent instead and pass change handlers:
import React from 'react'
function HelloWorld() {
return <Parent />
}
function Parent() {
const [msg, setMsg] = React.useState('hello')
return (
<>
<Brother setMsg={setMsg} />
<hr />
<Sister msg={msg} />
</>
)
}
function Brother(props) {
const { setMsg } = props
return <button onClick={() => setMsg('Yeah!')}>Click here</button>
}
function Sister(props) {
const { msg } = props
return <p>{msg}</p>
}
export default HelloWorld
The brother can pass a message to its sister by declaring the state in the parent. React takes care of updating and rendering.
You can change the port by running yarn dev --port 8080
.