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);
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();
}
...
}
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() {
...
}
}
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));
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>
);
};
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);
- It sets
state.currentCount
asinitialCount
similar to the Java constructor, and - returns a
setCurrentCount()
function that's equivalent to thesetState()
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]);
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));
}
}
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)