1. Functional components
There really is no clear winner between functional and class components. However, functional components are easier to read and test because they are plain JavaScript functions without state or lifecycle-hooks. You end up with less code, and typically this results in faster development. Not to mention with the arrival of hooks, react developers have had the ability to abstract state and other operations into reusable functions. That are easily implemented within functional components.
For example:
const MyComponent = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={increment}>Click me</button>
</div>
);
};
reads far better than
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
increment() {
this.setState((state) => ({
count: state.count + 1,
}));
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.increment()}>Click me</button>
</div>
);
}
}
Notice how both of these components do the same thing, but the functional component is much easier to read and understand. Not to mention it quite helpfully explains itself at the very top, so it reads much nicer top to bottom.
2. Hooks
We've had hooks for a while now they've been recieved insanely well by the react community at large, with good reason as well. They are simply awesome and make react development a real joy. They allow you to abstract state and functionallity into reusable functions that can be used within functional components. This is amazing for react developers as it allows us to write less code and have more control over our components. For example:
const useCounter = (initialState = 0) => {
const [count, setCount] = useState(initialState);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(initialState);
return { count, increment, decrement, reset };
};
This is a custom hook that allows us to abstract state and functionality into a reusable function. This can then be used within functional components like so:
const MyComponent = () => {
const { count, increment, decrement, reset } = useCounter(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={increment}>Click me</button>
</div>
);
};
3. Typescript
Typescript is a superset of javascript that adds static typing to javascript. If you're not using typescript yet or have never used a statically or storngly typed language before your first thought might be well what's the point isn't much easier to to have to worry about types. However, the benefits of using typescript are numerous and far outweigh the cons. Especially when working in a team of any size. Essentially it ensures that you and your team are on the same page when it comes to the types of data that are being passed around. This is especially important when working with react as it allows you to ensure that the data you're passing to your components is of the correct type. For example:
export interface IPerson {
name: string;
age: number;
}
export const Person = (props: React.PropsWithChildren<IPerson>) => {
const { name, age } = props;
return (
<div>
<h1>{name}</h1>
<p>{age}</p>
</div>
);
};
export default Person;
As you can see in the example above we define an interface that describes the type of data that we expect to be passed to our component. This ensures that age for example will always be a number and not a string, this is especially important if we expect certain functions to run on our props. You are all familiar with the good old
"100" - 1; // returns 99
"100" + 1; // returns 1001 because concatenation
Only you can prevent type errors.
4. State
You are probably already familiar with react state in some shape or form you've probably used the useState
hook in some shape or form or perhaps you've used Redux, maybe just maybe you even delved into libraries like zustand, recoil, jotai and so on. This is all fine and good, but what will really set you apart from the rest is not always thinking about state as just one singular thing, if anything there's 3 types of state.
Server State or Remote State
This is the state that lives somewhere on a remote server. It can change drastically and dramatically in any given time frame depending on the type of data it contains, e.g: say you wish to create a webapp that allows your users to view current stock or crypto values. We all know how much these things can fluctuate on a second by second basis. At a certain point you will have to think about how you wish to sync your client state to the remote state. This is where libraries like React Query and SWR come in handy, they make it easy for us to sync our client state with our remote state frequently and efficiently.
The wrong approach would be to simply cram all of this into our global state or application state, which brings us to:
Application State
This is the state of your application and only your application, the server does not need to know about any changes to this state. For example what belongs in here would be: current user, theme, user settings, currently playing(video/audio) etc.
This is were you would use a library like Redux, Zustand, Recoil or Jotai to manage your application state. You could even use something like the built in react Context, however I'd urge you to use Context only when you are not expecting your Context state too change very often. This is because Context is not very performant and can cause unnecessary re-renders. Plus it's a massive pain to use and test.
Local State or Component State
This is the state of your component and only your component. This is where you would use the useState
hook. This is where you would store things like: checkbox state, form values, modal state, dropdown state, etc.
This state should optimally live and die with your component.
5. Testing
Ah testing, we all know it's of vital importance and yet we never get around to doing it or if we do then we usually do it improperly. But trust me it's crazy how many headaches can be avoided by simply writing unit tests, let alone integration tests and e2e tests. How to write good tests and how to write them properly is a whole other topic, but I'll give you a few tips that will help you get started.
Unit Tests
When tackling unit tests always think small, what is the smallest and most vital bit of functionallity that I can test, this may be a hook, function or even a component. Also make sure to test your components in isolation, this means that you should mock any dependencies that your component may have. For example if you have a component that fetches data from an API, you should mock the API call and return some dummy data. This ensures that your test is not dependent on the API and will always return the same data.
Test Driven Development - TDD
TDD is a fantastic way to tackle complex problems as it forces you to break functionallity down into it's smallest parts. Basically in test driven development you flip the script on it's head and write the test for the expected result first and then and only then work on the implementation all the while optimally running your testing framework in "watch" mode so that it runs your tests every time you save a file. Keep coding out the functionallity of your function, hook or component untill the test passes. Tadah you've just done test driven development and after that dot goes from red to green you get a hit of dopamine that will keep you going for hours.
Integration Tests
Integration tests are a bit more complex than unit tests, but they are still relatively easy to write. Integration tests are used to test how different parts of your application interact with each other. For example if you have a component that fetches data from an API and then displays that data in a table, you would write an integration test that tests that the data is fetched from the API and then displayed in the table. This is a bit more complex than unit tests as you will have to mock the API call and return some dummy data. You will also have to mock the table component and return some dummy data. This ensures that your test is not dependent on the API and will always return the same data.
End to End Tests
End to end tests are the most complex of all tests, but they are also the most important. End to end tests are used to test how your application behaves from start to finish. For example if you have a component that fetches data from an API and then displays that data in a table, you would write an end to end test that tests that the data is fetched from the API and then displayed in the table. This is a bit more complex than unit tests as you will have to mock the API call and return some dummy data. You will also have to mock the table component and return some dummy data. This ensures that your test is not dependent on the API and will always return the same data.
6. Composition vs Inheritance
Composition is when we create multiple components with their own functionallity and then combine them together to create a more complex component. This is the preferred way of creating components in React.
Inheritance is when we create a component that inherits the functionallity of another component. This is not the preferred way of creating components in React.
Example of composition:
interface ICheckbox {
checked: boolean;
}
export const Checkbox = ({ checked }: ICheckbox) => {
return (
<div>
<input type="checkbox" value={checked} />
</div>
);
};
interface ISetting {
name: string;
value: boolean;
}
export const Setting = ({ name, value }: ISetting) => {
return (
<div>
<span>{name}</span>
<checkbox value={value} />
</div>
);
};
In the above example we have a checkbox component (albeit a bad one when it comes to reactivity) and a setting component that uses the checkbox component to display a setting. This is a good example of composition. We could take it even further by then creating a settingsList component that uses the setting component to display a list of settings. That same component could also use other components such as buttons, inputs, headings etc. all this via composition.
Example of inheritance:
export class Button extends React.Component {
render() {
return <button>{this.props.children}</button>;
}
}
export class PrimaryButton extends Button {
render() {
return <button className="primary">{this.props.children}</button>;
}
}
In the above example we created a button component named Button
and then another component named PrimaryButton
that inherits the functionallity of the Button
component. This may in the future make our life a bit more difficult as we may want to add a new functionallity to the Button
component, but we don't want that functionallity to be inherited by the PrimaryButton
component. Say for example we want to add a disabled
prop to the Button
component, but we don't want that prop to be inherited by the PrimaryButton
component.
7. Join the React Community
React has grown into an absolute behemoth and likewise the community around it has grown exponentially over the years. Now going at it solo is all fine and good, but you will find that you will hit a wall at some point and you will need help. This is where the community comes in. There are a ton of resources out there to help you get unstuck, but I'll give you a few of my favorites.
- Reactiflux - The official React Discord server
- Dan Abramov's Twitter - Dan is the creator of Redux and the author of the React docs
- React Twitter - The official React Twitter account
- Ben Awad - Ben is a full stack developer and React developer extraordinaire not to mention deadpan comedy expert
- React Docs - The official React docs explore them, they are a goldmine of information
8. Bonus: Hack around and have fun
This is the most important part of all, have fun. React is a fantastic tool and it's a joy to work with. So hack around, break things, make things, learn things, have fun. That's what it's all about.