DEV Community

TechBos😎 for getd.io/ - Postman without native apps

Posted on • Edited on

Thinking in Java: Learn React and React Hooks with Java Code ☕

As a long-time Java developer, it took me a while to understand some of the magics behind React. This post is my attempt to explain some of them in Java concepts. It's not meant to provide a strict mapping from Java to React.

Below is a React Counter component. It renders a count number, with a button to increment it. Every time the button is clicked, the count is added by one and the value is updated on the screen.

type Props = { initialCount: number };
type State = { currentCount: number };

class Counter extends React.Component<Props, State> {
  // Sets currentCount to initialCount when component is created
  state: State = { currentCount: this.props.initialCount };

  // Renders a text and a button, which increments count by one when clicked.
  render() {
    return (
      <div>
        {this.state.currentCount}
        <button onClick={() =>
          this.setState({ currentCount: this.state.currentCount + 1 })
        }>
          Increment
        </button>
      </div>
    );
  }
}

// Renders Counter at root
const rootElement = document.getElementById("root");
render(<Counter initialCount={0} />, rootElement);
Enter fullscreen mode Exit fullscreen mode

The same React component can be (sort-of) written in Java:

// The Props class to pass data into Counter, publicly construct-able.
public class Props {
  public final int initialCount;
  public Props(int initialCount) { this.initialCount = initialCount; }
}

public class Counter {
  // The State class to hold internal data of Counter, private only.
  private static class State {
    final int currentCount;
    State(int count) { this.currentCount = count; }
  }

  private State state;
  private Props props;
  private boolean shouldRender;

  // Constructor. Called once per component lifecycle.
  public Counter(final Props props) {
    this.updateProps(props);
    this.setState(new State(props.initialCount));
  }

  // Called by external whenever props have changed.
  public void updateProps(final Props props) {
    this.props = new Props(props.initialCount);
    this.shouldRender = true;
  }

  // Internal state update method for current count.
  private void setState(final State newState) {
    this.state = newState;
    this.shouldRender = true;
  }

  // Only allows render when shouldRender is true, i.e., props or state changed.
  public boolean shouldRender() {
    return this.shouldRender;
  }

  // Returns a 'virtal DOM' node 'Div' that contains a 'Text' node and a 'Button',
  // which increments count by one when clicked.
  public ReactNode render() {
    this.shouldRender = false;
    return new Div(
      new Text(this.state.currentCount),
      new Button("Increment", new OnClickHandler() {
        @Override
        public void onClick() {
          setState(new State(state.currentCount + 1));
        }
      });
    );
  }
}

// Renders Counter at root
public static void renderAt(HTMLElement root) {
  Counter counter = new Counter(new Props(0));
  root.addChild(counter);
  if (counter.shouldRender()) {
    counter.render();
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

To readers who have a Java background, table below maps some core React concepts into Java ones.

React Concept Java Concept
component class
props Passed-in parameters of constructor or updateProps() method, immutable internally
state A set of all private variables, immutable internally
setState() Replaces the previous group of private variables with a new group
render() Creates a new view with values applied

A few interesting things to note here:

props vs. state

In React, props are used for external world to communicate with the component, similar to Java constructor and public method parameters. In example above, it's used for setting its initial count value.

state, on the other hand, is used by the component internally, holding data that only matters to the component itself. This is similar to private variables in Java. However, a parent component's state can become a child component's props. E.g., Counter's currentCount is passed into Text component as the latter's props.

Both props and state should be immutables internally. In React, we never change their internal values directly. Instead, pass in a new props to the component (example below), and use setState() for setting a new state. Note how they are internally final in Java code above.

No Change, No Render

React only renders the component if either props or state has changed. This allows it to avoid unnecessary DOM updates. In above example, the component doesn't re-render until either button is clicked (a state change) or initialCount is changed (a props change). This is simulated using shouldRender() method above.

Virtual DOM nodes

render() returns virtual nodes. They are objects that describes how a certain type of UI should be rendered. They are not the end results. It's up to the React engine to decide how UI will be generated and presented on the screen. This allows React to work with different platforms. E.g., React.js renders a Html <button> while React Native renders an Android Button or iOS UIButton.

Handle props Changes

Now, let's briefly talk about React lifecycles. React provides several lifecycle methods. Today we take a look at componentDidUpdate().

Let's say we want the component to reset state.currentCount if the passed-in props.initialCount has changed. We can implement componentDidUpdate() as below:

class Counter extends React.Component<Props, State> {
  state: State = { currentCount: this.props.initialCount };

  // After props changed, check if initialCount has changed, then reset currentCount to the new initialCount.
  componentDidUpdate(prevProps: Props) {
    if (prevProps.initialCount !== this.props.initialCount) {
      this.setState({ currentCount: this.props.initialCount });
    }
  }

  render() {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

This may be written in Java as:

class Counter {
  ...
  // Called by external whenever props have changed.
  public void updateProps(final Props props) {
    final Props prevProps = this.props;
    this.props = new Props(props.initialCount);
    this.shouldRender = true;
    this.componentDidUpdate(prevProps);
  }

  private void componentDidUpdate(final Props prevProps) {
    if (prevProps.initialCount != this.props.initialCount) {
      setState(new State(this.props.initialCount));
    }
  }
  ...
}
Counter counter = new Counter(new Props(0));
counter.updateProps(new Props(100));
Enter fullscreen mode Exit fullscreen mode

The external world calls updateProps() to update Counter's props. Here, updateProps() preserves prevProps, and passes it into componentDidUpdate(). This allows the component to detect a props change and make updates accordingly.

Also note that setting new props doesn't require creating a new component instance. In the example above, the same Counter component is reused with new props. In fact, React tries to reuse existing components as much as possible using some smart DOM matching and the key props. It only creates new components when they cannot be found on the current DOM tree.

React Hooks

If you are learning React, you must learn Hooks as it's the new standard (a good thing). Let's quickly look at the equivalent code in React Hooks:

const Counter = ({ initialCount }: Props) => {
  const [currentCount, setCurrentCount] = React.useState(initialCount);

  React.useEffect(() => {
    setCurrentCount(initialCount);
  }, [initialCount]);

  return (
    <div>
      {currentCount}
      <button onClick={() => setCurrentCount(currentCount + 1)}>
        Increment
      </button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

The code is just much conciser because many things are hidden behind each line.

The line below uses React.useState(). It kills two birds with one stone (sorry, birds 🥺).

  const [currentCount, setCurrentCount] = React.useState(initialCount);
Enter fullscreen mode Exit fullscreen mode
  • It sets state.currentCount as initialCount similar to the Java constructor, and
  • returns a setCurrentCount() function that's equivalent to the setState() method used in Java.

The benefit of using this pattern is that you can break down one single state object into multiple simple values, each controlled by its own useState() method.

Next, the lines below uses React.useEffect() to create an effect, which is run every time the component updates.

  React.useEffect(() => {
    setCurrentCount(initialCount);
  }, [initialCount]);
Enter fullscreen mode Exit fullscreen mode

In this case, the effect is tied to the initialCount value (note the last parameter of useEffect()). This tells useEffect to only run setCurrentCount(initialCount) when initialCount changes. This is equivalent to Java code below:

  private void componentDidUpdate(final Props prevProps) {
    if (prevProps.initialCount != this.props.initialCount) {
      setState(new State(this.props.initialCount));
    }
  }
Enter fullscreen mode Exit fullscreen mode

There are many other magics in React and Hooks that go beyond this post. Leave a comment below if you'd like to learn more on this topic ❤️❤️❤️

Top comments (0)