Skip to main content

React State: Managing Numbers, Strings, and Booleans

The useState hook can manage any JavaScript data type, but mastering the three most fundamental ones — numbers, strings, and booleans — is the foundation of interactive React components. Numbers power counters and incrementers; booleans control visibility and conditional rendering; strings capture user input and drive form functionality. This article teaches you how to use primitive state types to build interactive, controlled components that respond instantly to user actions.

Key Takeaways

  • Numbers in state — use useState to manage numerical counters, scores, and incremental values; always use the updater function form when new state depends on previous state.
  • Booleans for toggling — boolean state is the engine of conditional rendering; use it for show/hide, enabled/disabled, and any binary state.
  • Strings for user input — link an input's value prop to state and its onChange handler to a state setter to create a controlled component.
  • Controlled components — when an input's value is controlled by React state, React becomes the single source of truth, ensuring predictable form behavior.
  • Updater functions — pass a function like (prev) => prev + 1 to state setters when new state depends on the old value; this guarantees you are working with the latest state.

How Do You Manage Number State with useState?

Numbers are one of the most common state types, appearing in counters, scores, progress bars, and any feature where a user increments or decrements a value.

import React, { useState } from 'react';

function StepTracker() {
const [steps, setSteps] = useState(0);

function handleIncrement() {
setSteps(prevSteps => prevSteps + 1);
}

return (
<div>
<h2>Today you've taken {steps} steps!</h2>
<button onClick={handleIncrement}>
I took another step
</button>
<button onClick={() => setSteps(0)}>
Reset Steps
</button>
</div>
);
}

export default StepTracker;

The useState(0) hook initializes the state with the value 0. Each time the button is clicked, handleIncrement() calls setSteps() with an updater function that adds 1 to the previous value. React then re-renders the component with the new count.

Using Updater Functions for Reliable State Updates

Always use the updater function form (prevSteps => prevSteps + 1) when the new state depends on the old value. This guarantees you are working with the most recent state, especially important if state is updated multiple times rapidly.

// Good: using updater function
setSteps(prevSteps => prevSteps + 1);

// Risky: direct value, may use stale state
setSteps(steps + 1);

The direct value form (setSteps(steps + 1)) works in simple cases but can lead to bugs if React batches multiple updates.

How Do You Use Boolean State for Conditional Rendering?

Boolean state is perfect for toggling UI elements, controlling visibility, and managing binary features like dark/light mode or notifications on/off.

import React, { useState } from 'react';

function Disclosure() {
const [isVisible, setIsVisible] = useState(true);

function toggleVisibility() {
setIsVisible(prevIsVisible => !prevIsVisible);
}

return (
<div>
<button onClick={toggleVisibility}>
{isVisible ? 'Hide' : 'Show'} Details
</button>

{isVisible && (
<p>
Here are some secret details that can be shown or hidden!
</p>
)}
</div>
);
}

export default Disclosure;

The toggle function inverts the boolean: !prevIsVisible flips true to false and vice versa. The conditional render isVisible && (<p>...</p>) displays the paragraph only when isVisible is true.

Conditional Rendering Patterns

{/* Logical AND: render if true */}
{isVisible && <p>Content</p>}

{/* Ternary: render different content */}
{isVisible ? <p>Visible</p> : <p>Hidden</p>}

{/* Nested ternaries for multiple states */}
{status === 'loading' ? <Spinner /> : status === 'error' ? <Error /> : <Content />}

The logical AND pattern is most common; ternaries work when you need different output for true and false.

How Do You Manage String State for Form Input?

String state captures text input from the user. To link an input to state, bind its value prop to the state variable and its onChange handler to a function that updates state.

import React, { useState } from 'react';

function Greeter() {
const [name, setName] = useState('');

function handleInputChange(event) {
setName(event.target.value);
}

return (
<div>
<label htmlFor="name-input">Enter your name: </label>
<input
id="name-input"
type="text"
value={name}
onChange={handleInputChange}
placeholder="Your name here"
/>

{name && <h3>Hello, {name}!</h3>}
</div>
);
}

export default Greeter;

The input's value={name} ensures the input always displays what is in state. The onChange={handleInputChange} handler fires on every keystroke and updates state with the new input value. This pattern is called a controlled component — React state is the single source of truth.

Controlled vs. Uncontrolled Components

  • Controlled: Input value is always bound to React state. You control when and how it updates. Predictable, testable, integrates with state management.
  • Uncontrolled: Input manages its own value (browser handles it). You access the value via a ref. Simpler for one-off inputs, but harder to manage multiple fields or validation.
// Controlled component
<input value={name} onChange={e => setName(e.target.value)} />

// Uncontrolled component
<input defaultValue="initial" ref={inputRef} />

Use controlled components in forms; use uncontrolled for simple one-off inputs or when integrating with non-React libraries.

How Do You Combine Multiple Primitive State Variables?

Real applications often manage multiple pieces of state together. Here is a user preferences panel that combines numbers, strings, and booleans:

import React, { useState } from 'react';

function UserPreferences() {
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState(true);
const [fontSize, setFontSize] = useState(16);

const panelStyle = {
backgroundColor: theme === 'light' ? '#FFF' : '#333',
color: theme === 'light' ? '#333' : '#FFF',
border: '1px solid #CCC',
padding: '20px',
borderRadius: '8px',
fontSize: `${fontSize}px`,
transition: 'all 0.3s ease',
};

return (
<div style={panelStyle}>
<h3>User Preferences</h3>

{/* String state: theme dropdown */}
<div>
<label>Theme: </label>
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>

{/* Boolean state: notifications toggle */}
<div style={{ marginTop: '15px' }}>
<label>
<input
type="checkbox"
checked={notifications}
onChange={e => setNotifications(e.target.checked)}
/>
Enable Notifications
</label>
</div>

{/* Number state: font size buttons */}
<div style={{ marginTop: '15px' }}>
<label>Font Size: {fontSize}px</label>
<button onClick={() => setFontSize(s => s - 1)}>-</button>
<button onClick={() => setFontSize(s => s + 1)}>+</button>
</div>
</div>
);
}

export default UserPreferences;

Each state variable is independent; updating one triggers a re-render with all current state values. This pattern scales well until state becomes complex, at which point you might consider useReducer or Context API.

What Are the Best Practices for Primitive State?

Initialize State Meaningfully

Initialize state with a sensible default that matches your component's purpose. An empty string for a form input, false for a disabled toggle, 0 for a counter.

const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [isActive, setIsActive] = useState(false);

Use Descriptive Variable Names

Name your state variable and setter clearly. [count, setCount] is obvious; [data, setData] is vague.

Keep State as Simple as Possible

Store only what you need. If fullName can be derived from firstName and lastName, do not store it; compute it. This reduces complexity and avoids synchronization bugs.

Use Multiple useState Calls, Not Objects

For independent pieces of primitive state, use multiple hooks rather than one state object. This keeps related logic together:

// Good: separate hooks for independent state
const [name, setName] = useState('');
const [age, setAge] = useState(0);

// Less ideal: bundling primitives in an object
const [user, setUser] = useState({ name: '', age: 0 });
// requires: setUser({ ...user, name: newName })

Frequently Asked Questions

Why should I use the updater function form of state setters?

The updater function (prev) => prev + 1 guarantees you are working with the latest state value, even if multiple updates are batched by React. This prevents bugs where state feels stale.

What is the difference between onChange and onInput?

onChange fires after a value is committed; onInput fires on every keystroke. In practice, they behave similarly in modern browsers. Use onChange for standard form handling.

Can I update multiple state variables in one event handler?

Yes. You can call multiple state setters in a single handler, and React will batch them into one re-render:

function handleReset() {
setCount(0);
setName('');
setIsActive(false);
}

What happens if I call a state setter with the same value?

React compares the old and new values. If they are identical, React skips the re-render for that state variable (though other state updates may still trigger one). This is an optimization.

When should I use conditional rendering vs. CSS display: none?

Use conditional rendering to completely remove elements from the DOM when they should not be shown. Use CSS display: none only if you need the element in the DOM for other reasons (e.g., measuring dimensions).

Further Reading