DEV Community

dsmith_sys
dsmith_sys

Posted on

React Core 1: Understanding the Basics

Introduction

React is a JavaScript library for building user interfaces. I don't want to talk too much about the introduction—you can Google it or ask an AI about it. Here in this blog, I'll focus on the core concepts you should understand to become comfortable with React. I hope you'll stick around until the end.

JSX & How React Renders UI

JSX is a syntax extension for JavaScript that allows us to write HTML-like markup inside JavaScript.

For example:

function App() {
  return (
    <div>
      <h1>Hello World</h1>
      <button>Click me</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The browser does not directly understand JSX syntax. During the development/build process, our build tools transform JSX into regular JavaScript that the browser can execute.

You can think of the flow roughly like this:

JSX + JavaScript source code
          ↓
     Build tooling
          ↓
JavaScript/CSS/assets the browser can load
          ↓
        Browser
          ↓
      React starts
          ↓
React renders your components
          ↓
React reconciles the UI
          ↓
Necessary DOM updates
          ↓
      Browser paints
          ↓
       You see the UI
Enter fullscreen mode Exit fullscreen mode

For example, when the browser loads a typical React application, it first loads the HTML file. The HTML contains something like:

<div id="root"></div>
Enter fullscreen mode Exit fullscreen mode

Then the JavaScript application runs and React mounts the application into that root:

createRoot(document.getElementById("root")).render(<App />);
Enter fullscreen mode Exit fullscreen mode

React then uses your components to determine what the UI should look like.

You may have heard the term Virtual DOM. It is useful as a mental model for understanding that React maintains an internal representation of the UI and uses reconciliation to determine what needs to change in the actual DOM. The browser itself does not know about React's Virtual DOM.

The important flow to remember is:

State/props change
       ↓
React renders the component again
       ↓
React gets a new description of the UI
       ↓
React reconciles it with the previous one
       ↓
React commits the necessary DOM changes
Enter fullscreen mode Exit fullscreen mode

Element vs Component

This is pretty simple.

Consider this:

function App() {
  return (
    <div>
      <h1>Hello World</h1>
      <button>Click me</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

<h1>, <button>, and <div> are JSX that create React elements.

App is a component.

A React element is an immutable plain object that describes what should appear in the UI. A component is a function (or class) that returns React elements.

You can think of it like this:

Component
    ↓
returns
    ↓
React elements
    ↓
React uses them to determine the UI
Enter fullscreen mode Exit fullscreen mode

So:

function App() {
  return <h1>Hello World</h1>;
}
Enter fullscreen mode Exit fullscreen mode

App → component

<h1>Hello World</h1> → React element created from JSX

Props vs State

Let's talk about two things you'll use constantly in React: props and state.

Props

Props are inputs passed from a parent component to a child component.

They are similar to function arguments.

For example:

function User({ name }) {
  return <h1>Hello {name}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

And the parent can pass the prop like this:

<User name="Rahul" />
Enter fullscreen mode Exit fullscreen mode

You can think of it roughly like:

Parent
   ↓
passes props
   ↓
User({ name: "Rahul" })
Enter fullscreen mode Exit fullscreen mode

Props are read-only from the child component's perspective. A child should not modify its props directly.

State

State is data managed by React that belongs to a component instance and can change over time.

For example:

const [count, setCount] = useState(0);
Enter fullscreen mode Exit fullscreen mode

Here:

  • count is the current state value.
  • setCount is the function we use to request an update to that state.

When the state changes, React schedules a re-render.

One very important thing: a re-render does not mean React destroys the state and creates it again.

For example:

function Counter() {
  const [count, setCount] = useState(0);

  console.log("render");

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Initially:

count = 0
   ↓
<h1>0</h1>
Enter fullscreen mode Exit fullscreen mode

After clicking the button:

setCount(1)
   ↓
React schedules a render
   ↓
Counter runs again
   ↓
count = 1
   ↓
<h1>1</h1>
Enter fullscreen mode Exit fullscreen mode

The state is preserved between renders.

A re-render simply means React runs the component again to determine what the UI should look like with the current props and state.

Also, don't confuse a re-render with a page reload. A page reload reloads the entire web page, while a React re-render is part of React's normal rendering process.

React state is stored in memory. If a component is removed from the React tree, its state is no longer preserved. A full page reload also resets in-memory React state.

If you need data to survive a page reload, you need some form of persistence, such as localStorage, a database, or another storage mechanism.

State and props should be treated as immutable

You should not directly mutate state:

count = count + 1;
Enter fullscreen mode Exit fullscreen mode

Instead, use the setter:

setCount(count + 1);
Enter fullscreen mode Exit fullscreen mode

Similarly, don't directly modify props inside a child component.

So remember:

Props are read-only from the child's perspective, and state should be treated as immutable and updated through its setter.

Now let's put props and state together.

Try to run this example and follow the whole flow of execution from start to end. You'll get a much better idea of how they work together:

function Counter({ name }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>
        {name}: {count}
      </h1>

      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
    </div>
  );
}

function App() {
  return <Counter name="Rahul" />;
}
Enter fullscreen mode Exit fullscreen mode

Here, name comes from props, while count is managed using state.

Controlled & Uncontrolled Components

This concept mostly comes up with form inputs.

Controlled components

An input is controlled when React state controls its value.

For example:

function Form() {
  const [name, setName] = useState("");

  return (
    <input
      value={name}
      onChange={(e) => setName(e.target.value)}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

The flow is:

User types
    ↓
onChange runs
    ↓
setName(...)
    ↓
React state changes
    ↓
React re-renders
    ↓
input receives the new value
Enter fullscreen mode Exit fullscreen mode

Here, React is the source of truth for the input's value.

Uncontrolled components

An input is uncontrolled when the DOM itself maintains the current value.

For example:

function Form() {
  const inputRef = useRef(null);

  function handleSubmit() {
    console.log(inputRef.current.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} />

      <button type="submit">
        Submit
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, React isn't storing the input value in state. The DOM maintains it.

useRef gives us a way to access the DOM node:

inputRef.current
Enter fullscreen mode Exit fullscreen mode

Important: useRef itself does not make an input uncontrolled. The input is uncontrolled because its value is maintained by the DOM rather than being driven by React state.

Also remember:

useState
→ changing state causes a re-render

useRef
→ changing ref.current does not cause a re-render
Enter fullscreen mode Exit fullscreen mode

A ref can also store mutable values that persist across renders.

So the simple rule is:

Use controlled components when you want React to control the input value through state. Use uncontrolled components when you want the DOM to manage the input value.

TASK: Write a simple form using both a controlled and an uncontrolled input. Submit the form and compare how you access their values.

key Prop in Lists

The key prop is needed when we're rendering multiple elements from a collection.

It helps React identify which items have changed, been added, removed, or moved between renders.

For example:

const numbers = [1, 2, 3, 4, 5];

const listItems = numbers.map((number) => (
  <li key={number.toString()}>
    {number}
  </li>
));
Enter fullscreen mode Exit fullscreen mode

Keys give elements a stable identity:

key = 1 → first item
key = 2 → second item
key = 3 → third item
Enter fullscreen mode Exit fullscreen mode

Keys should be unique among siblings. They don't need to be globally unique across your entire application.

For example:

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 3, name: "Charlie" }
];

const userElements = users.map((user) => (
  <p key={user.id}>
    {user.name}
  </p>
));
Enter fullscreen mode Exit fullscreen mode

Notice that the elements are <p> elements, not <li> elements. The key prop is not specific to lists or <li>. It is needed when React is dealing with multiple dynamically generated siblings.

What about array indexes?

You might see this:

users.map((user, index) => (
  <User key={index} user={user} />
));
Enter fullscreen mode Exit fullscreen mode

It works, but it can cause bugs when the list can be reordered, inserted into, or have items removed.

For example:

Before:

key=0 → Alice
key=1 → Bob
key=2 → Charlie
Enter fullscreen mode Exit fullscreen mode

If Alice is removed:

After:

key=0 → Bob
key=1 → Charlie
Enter fullscreen mode Exit fullscreen mode

The keys stayed the same, but the items associated with those keys changed.

If those components contain their own state, React may preserve the state of the old component instance and associate it with a different item.

That's why a stable ID from your data is usually a better key:

<User key={user.id} user={user} />
Enter fullscreen mode Exit fullscreen mode

Using an index can be okay when the list is static and its order/membership never changes.

Also, don't generate keys like this:

key={Math.random()}
Enter fullscreen mode Exit fullscreen mode

or:

key={Date.now()}
Enter fullscreen mode Exit fullscreen mode

Those values can change between renders. React can then treat the element as a completely new element, causing component instances to be recreated and potentially losing their state.

NOTE: The goal isn't simply "make the key unique." The key should be stable and unique among siblings.

Lifting State Up

This is a pretty interesting and useful React pattern.

Imagine you have two child components, and both of them need to share the same piece of state.

For example:

        Parent
       /      \
   Child A   Child B
Enter fullscreen mode Exit fullscreen mode

If both children need access to count, where should count live?

We can lift the state up to their closest common ancestor:

        Parent
        count
       /     \
      ↓       ↓
 Child A   Child B
Enter fullscreen mode Exit fullscreen mode

The parent owns the state and passes the required data and update functions down through props.

For example:

function ParentCounter() {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>

      <ChildBlue value={count} />
      <ChildGreen value={count} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now both children receive the same count:

function ChildBlue({ value }) {
  return <p>{value}</p>;
}

function ChildGreen({ value }) {
  return <p>{value}</p>;
}
Enter fullscreen mode Exit fullscreen mode

The important idea is:

Move shared state to the closest common ancestor of the components that need it.

This doesn't mean we should move every piece of state all the way up to <App />.

If two components can share state through a lower-level parent, keep the state there.

Moving state unnecessarily high can lead to unnecessary prop drilling and make state ownership harder to understand.

TASK: Build a simple counter app with two child components. The first child should display the count in blue, while the second displays it in green. Keep the count state in their parent and pass it down through props.

What about Context?

Now imagine that the component that needs the state is deeply nested:

Parent
  ↓
Child
  ↓
AnotherChild
  ↓
AnotherChild
  ↓
DeepChild ← needs the data
Enter fullscreen mode Exit fullscreen mode

Passing the same prop through every component can become annoying. This is called prop drilling.

In this situation, we can use the Context API.

Context allows a value to be made available to components within a particular provider subtree without manually passing it through every intermediate component.

For example:

Provider
   ↓
   ├── Component A
   │
   ├── Component B
   │      ↓
   │   Component C
   │      ↓
   │   DeepChild ← useContext()
   │
   └── Component D
Enter fullscreen mode Exit fullscreen mode

Context isn't necessarily "global state." It is better to think of it as a way to make a value available to a subtree of components.

Also, Context doesn't manage state by itself.

You can combine Context with useState:

const [user, setUser] = useState(null);

<UserContext.Provider value={{ user, setUser }}>
  <App />
</UserContext.Provider>
Enter fullscreen mode Exit fullscreen mode

Here:

useState
   ↓
manages the state

Context
   ↓
makes that state available to descendants
Enter fullscreen mode Exit fullscreen mode

So don't use Context for every piece of state. If two nearby components need to share state, lifting the state up and passing props is often simpler.

Context becomes useful when many components in a subtree need the same value and passing props through multiple levels becomes inconvenient.

Conclusion

That's it for React Core 1.

We've covered some of the concepts that form the foundation of React:

  • JSX
  • How React renders UI
  • Elements vs Components
  • Props vs State
  • Controlled vs Uncontrolled Components
  • useState vs useRef
  • The key prop
  • Lifting State Up
  • Context and Prop Drilling

i think this is enough for now, it covers mostly the basics of react. In the next blog, we'll cover more advanced topics, it will be interesting, so stay tuned.

Happy coding!

Top comments (4)

Collapse
 
markmuslimov profile image
Mark

Wow, thank for sharing your knowledge. I'm currently at the CSS part about to learn Java Script. I plan to learn React after I master the basics of the Java Script. I'll keep this in my favorites to have this as a resource I can learn!

Collapse
 
emt2dev profile image
David Duron

Don't quit

Collapse
 
emt2dev profile image
David Duron

The author presented the important aspects of learning react in a way that developers with experience in different SPA frameworks would be able to easily understand the content.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.