Skip to content

State Management Patterns

This guide documents our established patterns for state management, using real examples from our codebase.

Use this flowchart to determine the appropriate state management approach:

Key Decision Points:

  • Scope: Where is the state needed?
  • Type: What kind of data are you storing?
  • Access Pattern: How is the state used?

Common Pitfalls:

  • Using shared state for component-local data
  • Complex state structures in atoms
  • Missing loading/error states for async state
  • Inconsistent state update patterns

We follow specific patterns for managing state in our components. For naming conventions, see our Naming Conventions guide.

For component-local state, we use SolidJS’s createSignal:

components/latestArticles/latestArticles.tsx
import { createSignal, createMemo } from "solid-js";
export function LatestArticles(props: LatestArticlesProps) {
const [activeTag, setActiveTag] = createSignal(props.defaultTag);
const [loading, setLoading] = createSignal(false);
const [filteredArticles, setFilteredArticles] = createSignal(props.articles);
// Derived state using createMemo
const visibleArticles = createMemo(() =>
filteredArticles().filter(article =>
article.tags.includes(activeTag())
)
);
const handleTagChange = async (tag: string) => {
setLoading(true);
setActiveTag(tag);
try {
const articles = await fetchArticlesByTag(tag);
setFilteredArticles(articles);
} catch (err) {
console.error("Failed to fetch articles", {
error: err.message,
component: "LatestArticles"
});
} finally {
setLoading(false);
}
};
return (
<div>
<TagSelector
active={activeTag()}
onChange={handleTagChange}
/>
<Show
when={!loading()}
fallback={<LoadingSpinner />}
>
<ArticleList articles={visibleArticles()} />
</Show>
</div>
);
}

🤔 Why This Pattern?

  • Fine-grained reactivity
  • Automatic dependency tracking
  • Efficient updates
  • Clear state ownership

For more complex state, use multiple signals with clear responsibilities:

components/fullScreenGallery/fullScreenGallery.tsx
import { createSignal, createEffect, onCleanup, onMount } from "solid-js";
export function FullScreenGallery(props: GalleryProps) {
// UI State
const [hasScroll, setHasScroll] = createSignal(false);
const [galleryRef, setGalleryRef] = createSignal<HTMLUListElement>();
const [activeChild, setActiveChild] = createSignal(props.currentImageIndex);
// Scroll Position State
const [scrollPosition, setScrollPosition] = createSignal({
left: 0,
behavior: "smooth" as ScrollBehavior,
});
// Side Effects
createEffect(() => {
const gallery = galleryRef();
if (!gallery) return;
const updateScroll = () => {
setHasScroll(gallery.scrollWidth > gallery.clientWidth);
};
updateScroll();
window.addEventListener("resize", updateScroll);
onCleanup(() => window.removeEventListener("resize", updateScroll));
});
return (
<div>
<ul ref={setGalleryRef}>
{/* Gallery items */}
</ul>
<Show when={hasScroll()}>
<NavigationControls
onNext={() => setActiveChild(prev => prev + 1)}
onPrev={() => setActiveChild(prev => prev - 1)}
/>
</Show>
</div>
);
}

🤔 Why This Pattern?

  • Separated concerns
  • Clear state updates
  • Lifecycle management
  • Cleanup handling

For forms, combine signals with validation and error states:

components/newsletterForm/newsletterForm.tsx
import { createSignal } from "solid-js";
export function NewsletterForm(props: NewsletterFormProps) {
const [email, setEmail] = createSignal("");
const [success, setSuccess] = createSignal(false);
const [error, setError] = createSignal("");
const [hasFocused, setHasFocused] = createSignal(false);
const handleSubmit = async (e: Event) => {
e.preventDefault();
try {
await submitNewsletter({
email: email(),
source: props.source
});
setSuccess(true);
setError("");
} catch (err) {
setError(err.message);
setSuccess(false);
}
};
return (
<form onSubmit={handleSubmit}>
<Show when={error()}>
<p mode="critical" class="text-primary">{error()}</p>
</Show>
<Show when={success()}>
<p mode="success" class="text-primary">Successfully subscribed!</p>
</Show>
{/* Form fields */}
</form>
);
}

🤔 Why This Pattern?

  • Clear form state
  • Error handling
  • Success feedback
  • Focus tracking
  1. Signal Creation

    • Create signals at component initialization
    • Use descriptive names (e.g., isLoading not flag)
    • Initialize with sensible defaults
    • Group related signals
  2. State Updates

    • Update state immutably
    • Batch related updates
    • Handle errors appropriately
    • Clean up side effects
  3. Derived State

    • Use createMemo for computed values
    • Keep computations pure
    • Minimize recomputation
    • Clear dependencies
  4. Component Organization

    • Co-locate state with usage
    • Split complex state logic
    • Handle cleanup in onCleanup
    • Document state shape

Don’t: Mutate signal values directly

// Bad: Direct mutation
const items = createSignal([]);
items()[0] = newItem; // Don't do this!

Do: Update immutably

// Good: Immutable updates
const [items, setItems] = createSignal([]);
setItems(prev => [...prev, newItem]);

Don’t: Create signals in loops or conditions

// Bad: Conditional signal creation
if (condition) {
const [state, setState] = createSignal(0);
}

Do: Create signals at component initialization

// Good: Consistent signal creation
const [state, setState] = createSignal(condition ? 0 : null);

Don’t: Use signals for static values

// Bad: Unnecessary signal
const [config, setConfig] = createSignal({
apiUrl: "https://api.example.com",
});

Do: Use regular constants

// Good: Static values
const config = {
apiUrl: "https://api.example.com",
};

When implementing state management, ask:

  1. State Location

    • Is this state truly needed?
    • Should it be component-local?
    • Does it need to be shared?
    • What’s the state lifetime?
  2. State Structure

    • Is the state minimal?
    • Are updates efficient?
    • Is derived state used?
    • Is cleanup needed?
  3. State Updates

    • Are updates immutable?
    • Is error handling in place?
    • Are side effects managed?
    • Is state initialized properly?
  4. Performance

    • Are computations optimized?
    • Is reactivity granular?
    • Are updates batched?
    • Is cleanup implemented?