Skip to main content

Next.js

note

Here are the steps you must take to get set up Kea with Next.js. For generic installation instructions, click here.

This guide might not be fully up to date. Please submit a PR if you find something that has changed.

Install the packages

In addition to the kea, you must also install the babel-plugin-kea and next-redux-wrapper packages:

# if you're using yarn
yarn add kea next-redux-wrapper
yarn add --dev babel-plugin-kea

# if you're using npm
npm install kea next-redux-wrapper --save
npm install babel-plugin-kea --save-dev

Set up babel-plugin-kea

In order to properly hydrate your store between server and client renders, we must install the babel-plugin-kea package. This ensures that every kea() call automatically gets a path, which help us link the same logic on the client and the server.

Add this to your .babelrc:

{
"presets": ["next/babel"],
"plugins": ["babel-plugin-kea"]
}

Create _app.js

Create pages/_app.js and paste in the following content. The content below is taken from next-redux-wrapper's readme. Notable changes have been annotated with comments.

// pages/_app.js
import React from 'react'
import { resetContext } from 'kea'
import App from 'next/app'
import withRedux from 'next-redux-wrapper'

// How long do we try to render on the server before giving up? In milliseconds.
const SERVER_RENDER_TIMEOUT = 1000

const makeStore = (initialState, options) => {
// Reset Kea's context and pass `initialState` as defaults.
// Add all plugins and other configuration here.
const context = resetContext({ defaults: initialState })
return context.store
}

class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
const pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {}

// In case we are server rendering and our `Component` has a `logic` attached
// to it, initialize it through a promise and pass the `resolve` function to
// the logic as props.

// It's then up to the logic to call `props.resolve()` when it has finished
// its setup.

if (ctx.isServer && Component.logic) {
await Promise.race([
new Promise((resolve) => Component.logic({ resolve }).mount()),
new Promise((resolve) => setTimeout(resolve, SERVER_RENDER_TIMEOUT)),
])
}

return { pageProps }
}

render() {
const { Component, pageProps, store } = this.props
return (
<Component {...pageProps} />
)
}
}
export default withRedux(makeStore)(MyApp)

Usage

Feel free to use kea calls anywhere in your app now!

If you have a logic that initializes the data for a page, pass it as the logic property on the page's Component: MyPage.logic = logic.

When this logic is built, it'll get a function resolve as part of props. Once your logic has finished initializing or it crashed, call props.resolve() to signal that the loading has finished.

import fetch from 'isomorphic-unfetch' // 👈 can't use window.fetch anymore

const logic = kea({
// ... skipping actions and reducers
events: ({ actions }) => ({
afterMount: [actions.fetchRepositories],
}),

listeners: ({ actions, props }) => ({
fetchRepositories: async () => {
try {
const response = await fetch(`https://api.github.com/users/keajs/repos`)
actions.setRepositories(await response.json())
} catch (error) {
actions.setFetchError(error.message)
}
props.resolve && props.resolve() // 👈 Resolved!
},
}),
})

function Github() {
const { repositories } = useValues(logic)
return (
<ul>
{repositories.map((repo) => (
<li>{repo.full_name}</li>
))}
</ul>
)
}

Github.logic = logic // 👈 Attach the logic on the Github component

export default Github

Sample app

Here is the Github API tutorial app, but adapted to work with Next.js server rendering. All the changes are highlighted with a finger. 👈

import React from 'react'
import {
kea,
actions,
reducers,
selectors,
listeners,
afterMount,
useActions,
useValues,
} from 'kea'
import fetch from 'isomorphic-unfetch' // 👈 can't use window.fetch anymore

const API_URL = 'https://api.github.com'

const logic = kea([
actions({
setUsername: (username) => ({ username }),
setRepositories: (repositories) => ({ repositories }),
setFetchError: (message) => ({ message }),
}),

reducers({
username: [
'keajs',
{
setUsername: (_, payload) => payload.username,
},
],
repositories: [
[],
{
setUsername: () => [],
setRepositories: (_, payload) => payload.repositories,
},
],
isLoading: [
true,
{
setUsername: () => true,
setRepositories: () => false,
setFetchError: () => false,
},
],
error: [
null,
{
setUsername: () => null,
setFetchError: (_, payload) => payload.message,
},
],
}),

selectors({
sortedRepositories: [
(selectors) => [selectors.repositories],
(repositories) => repositories.sort((a, b) => b.stargazers_count - a.stargazers_count),
],
}),

afterMount(({ actions, values }) => {
// Only load data on the client if it's not already there 👈
if (values.repositories.length === 0 && !values.error) {
actions.setUsername(values.username)
}
}),

listeners(({ actions, props }) => ({
setUsername: async ({ username }, breakpoint) => {
if (!props.resolve) {
// 👈 no need to debounce the server's response
await breakpoint(300)
}

try {
const url = `${API_URL}/users/${username}/repos?per_page=250`
const response = await fetch(url)
breakpoint()

const json = await response.json()

if (response.status === 200) {
actions.setRepositories(json)
} else {
actions.setFetchError(json.message)
}
} catch (error) {
actions.setFetchError(error.message)
}

props.resolve && props.resolve() // 👈 Resolved!
},
})),
])

// No difference here
function Github() {
const { username, isLoading, repositories, sortedRepositories, error } = useValues(logic)
const { setUsername } = useActions(logic)

return (
<div className="example-github-scene">
<div style={{ marginBottom: 20 }}>
<h1>Search for a github user</h1>
<input value={username} type="text" onChange={(e) => setUsername(e.target.value)} />
</div>
{isLoading ? (
<div>Loading...</div>
) : repositories.length > 0 ? (
<div>
Found {repositories.length} repositories for user {username}!
{sortedRepositories.map((repo) => (
<div key={repo.id}>
<a href={repo.html_url} target="_blank">
{repo.full_name}
</a>
{' - '}
{repo.stargazers_count} stars, {repo.forks} forks.
</div>
))}
</div>
) : (
<div>{error ? `Error: ${error}` : 'No repositories found'}</div>
)}
</div>
)
}

Github.logic = logic // 👈 Attach the logic on the Github component

export default Github

Questions & Answers

Ask questions about this page here.