State Management Patterns
This guide documents our established patterns for state management, using real examples from our codebase.
Quick Decision Guide
Section titled “Quick Decision Guide”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
State Types
Section titled “State Types”Component-Level State
Section titled “Component-Level State”We follow specific patterns for managing state in our components. For naming conventions, see our Naming Conventions guide.
SolidJS Signals
Section titled “SolidJS Signals”For component-local state, we use SolidJS’s createSignal:
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
Complex Component State
Section titled “Complex Component State”For more complex state, use multiple signals with clear responsibilities:
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
Form State Management
Section titled “Form State Management”For forms, combine signals with validation and error states:
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
Best Practices
Section titled “Best Practices”-
Signal Creation
- Create signals at component initialization
- Use descriptive names (e.g.,
isLoadingnotflag) - Initialize with sensible defaults
- Group related signals
-
State Updates
- Update state immutably
- Batch related updates
- Handle errors appropriately
- Clean up side effects
-
Derived State
- Use
createMemofor computed values - Keep computations pure
- Minimize recomputation
- Clear dependencies
- Use
-
Component Organization
- Co-locate state with usage
- Split complex state logic
- Handle cleanup in
onCleanup - Document state shape
Anti-Patterns to Avoid
Section titled “Anti-Patterns to Avoid”❌ Don’t: Mutate signal values directly
// Bad: Direct mutationconst items = createSignal([]);items()[0] = newItem; // Don't do this!✅ Do: Update immutably
// Good: Immutable updatesconst [items, setItems] = createSignal([]);setItems(prev => [...prev, newItem]);❌ Don’t: Create signals in loops or conditions
// Bad: Conditional signal creationif (condition) { const [state, setState] = createSignal(0);}✅ Do: Create signals at component initialization
// Good: Consistent signal creationconst [state, setState] = createSignal(condition ? 0 : null);❌ Don’t: Use signals for static values
// Bad: Unnecessary signalconst [config, setConfig] = createSignal({ apiUrl: "https://api.example.com",});✅ Do: Use regular constants
// Good: Static valuesconst config = { apiUrl: "https://api.example.com",};Decision Checklist
Section titled “Decision Checklist”When implementing state management, ask:
-
State Location
- Is this state truly needed?
- Should it be component-local?
- Does it need to be shared?
- What’s the state lifetime?
-
State Structure
- Is the state minimal?
- Are updates efficient?
- Is derived state used?
- Is cleanup needed?
-
State Updates
- Are updates immutable?
- Is error handling in place?
- Are side effects managed?
- Is state initialized properly?
-
Performance
- Are computations optimized?
- Is reactivity granular?
- Are updates batched?
- Is cleanup implemented?