Skip to content
Click Here to get a free report on how to make your website faster
RSS
State Management in Solid JS

By

Raqueebuddin Aziz

October 17, 2022 (Updated on May 4, 2023)

Freelance Web Designer & Developer

State Management in Solid JS

A guide on state management in a solid-js application. I will be using solid-start in this guide for convenience but all these concepts can be applied to any solid-js project.

This guide assumes you have gone through the solidjs tutorial.

Sharing State

True Global State

This is the one of the easiest thing to do in solid-js. Define a signal or store in an external file and import it into your components and all the components will have the same state and any changes will be propagated to all the components.

Example

/* src/state/index.js */
import { createSignal } from 'solid-js'
import { createStore } from 'solid-js/store'

const [count, setCount] = createSignal(0)
const [appState, setAppState] = createStore({
	title: 'My Awesome App',
	user: {
		name: 'John Doe',
		age: Infinity
	}
})

export const useAppState = () => [appState, setAppState]
export const useCount = () => [count, setCount]
/* src/components/Counter.jsx */
import { useCount } from '~/state'

export function Counter() {
	const [count, setCount] = useCount()

	return <div>{count()}</div>
}
/* src/components/Increment.jsx */
import { useCount } from '~/state'

export function Increment() {
	const [count, setCount] = useCount()

	return <button onClick={() => setCount(count() + 1)}>Make the count go up</button>
}
/* src/routes/index.jsx */
import { Counter } from '~/components/Counter'
import { Increment } from '~/components/Increment'

export default function Home() {
	return (
		<>
			<Counter />
			<Increment />
		</>
	)
}

In the example above even though the Increment and Counter components are separate, pressing the Increment component will still increment the visible count in the Counter component because both components use the same signal from src/state/index.js.

Context Aware Global State

Another way to share state is to use the createContext and useContext API. It’s basically like creating state in another file and importing it where you want to use it, but the key difference is you can scope it to specific parts of your project and even have different values simultaneously for different parts of your project.

User data is a perfect example where you want to use context API rather than true global state as with true global state you might leak user state across different users but with context API you can have different values scoped to each user.

Example

/* src/state/index.js */
import { createContext, createSignal, useContext } from 'solid-js'
import { createStore } from 'solid-js/store'

const [appState, setAppState] = createStore({
	title: 'My Awesome App',
	user: {
		name: 'John Doe',
		age: Infinity
	}
})

export const useAppState = () => [appState, setAppState]

const [count, setCount] = createSignal(0)
export const CountContext = createContext([
	count,
	setCount,
	{
		increment() {
			setCount((c) => c + 1)
		}
	}
])

export function CountProvider(props) {
	const [count, setCount] = createSignal(props.initialValue ?? 0)
	return (
		<CountContext.Provider
			value={[
				count,
				setCount,
				{
					increment() {
						setCount((c) => c + 1)
					}
				}
			]}
		>
			{props.children}
		</CountContext.Provider>
	)
}

export const useCount = () => useContext(CountContext)
/* src/components/Counter.jsx */
import { useCount } from '~/state'

export function Counter() {
	const [count, setCount] = useCount()

	return <div>{count()}</div>
}
/* src/components/Increment.jsx */
import { useCount } from '~/state'

export function Increment() {
	const [count, setCount, { increment }] = useCount()

	return <button onClick={() => increment()}>Make the count go up</button>
}
/* src/routes/index.jsx */
import { Counter } from '~/components/Counter'
import { Increment } from '~/components/Increment'
import { CountProvider } from '~/state'

export default function Home() {
	return (
		<>
			<CountProvider>
				<Counter />
				<Increment />
			</CountProvider>
			<CountProvider initialValue={10}>
				<Counter />
				<Increment />
			</CountProvider>
		</>
	)
}

Mutations

  • When using context API you can just include the mutations in the context provider like increment in the above example.
  • When using true global state you can just write a function in the state file and import it in other files, and then you have centralized mutations.

Example

/* src/state/index.js */
import { createSignal } from 'solid-js'
import { createStore } from 'solid-js/store'

const [count, setCount] = createSignal(0)
export const incrementCount = () => setCount(count() + 1)

const [appState, setAppState] = createStore({
	title: 'My Awesome App',
	user: {
		name: 'John Doe',
		age: Infinity
	},
	setTitle(val) {
		setAppState('title', val)
	}
})

export const useAppState = () => [appState, setAppState]
export const useCount = () => [count, setCount]
/* src/components/Increment.jsx */
import { incrementCount } from '~/state'

export function Increment() {
	return <button onClick={() => incrementCount()}>Make the count go up</button>
}
/* src/routes/index.jsx */
import { Counter } from '~/components/Counter'
import { Increment } from '~/components/Increment'
import { useAppState } from '~/state'

export default function Home() {
	const [appState, setAppState] = useAppState()
	setTimeout(() => appState.setTitle('More awesome app'), 4000)

	return (
		<>
			<div>{appState.title}</div>
			<Counter />
			<Increment />
		</>
	)
}

Async State

Suspense and createResource are your best friends while working with async state.

Define a resource in the state file and any relevant mutations and optionally (but highly recommended) when using the value wrap it in a Suspense to show a loading state.

Example

/* src/state/index.js */
import { createResource, createSignal } from 'solid-js'
import { createStore } from 'solid-js/store'

const [count, setCount] = createSignal(0)
export const incrementCount = () => setCount(count() + 1)

const [appState, setAppState] = createStore({
	title: 'My Awesome App',
	user: {
		name: 'John Doe',
		age: Infinity
	},
	setTitle(val) {
		setAppState('title', val)
	}
})

const [pokemon, { refetch }] = createResource((_, { refetching: name }) =>
	fetch(`https://pokeapi.co/api/v2/pokemon/${name || 'ditto'}`).then((res) => res.json())
)

export const useAppState = () => [appState, setAppState]
export const useCount = () => [count, setCount]
export const usePokemon = () => [pokemon, refetch]
/* src/routes/index.jsx */
import { createEffect, createSignal, Suspense } from 'solid-js'
import { Counter } from '~/components/Counter'
import { Increment } from '~/components/Increment'
import { usePokemon, useAppState } from '~/state'

export default function Home() {
	const [appState, setAppState] = useAppState()
	const [pokemon, fetchPokemon] = usePokemon()
	const [query, setQuery] = createSignal('')
	setTimeout(() => appState.setTitle('More awesome app'), 4000)

	return (
		<>
			<div>{appState.title}</div>
			<Counter />
			<Increment />
			<form
				onSubmit={(e) => {
					e.preventDefault()
					fetchPokemon(query())
				}}
			>
				<input onInput={(e) => setQuery(e.currentTarget.value)} value={query()} required />
			</form>
			<Suspense fallback={<div>loading...</div>}>
				<span>{pokemon()?.name || 'Not Found'}</span>
				<img src={pokemon()?.sprites.front_default} />
			</Suspense>
		</>
	)
}

Conclusion

Truth be told solid-js is a state management library that happens to render.

How do you like to manage state in your applications? Leave a comment down below

Tweet Share on LinkedIN Post on Reddit

Looking for a dev to help build an MVP for your tech startup?

Reach out to me.

Contact

We use cookies to enhance your browsing experience and analyze website traffic. By continuing to use our website, you consent to the use of cookies in accordance with our Privacy Policy.

Privacy Policy