Rename language articles
This commit is contained in:
51
snippets/react/s/breaking-react.md
Normal file
51
snippets/react/s/breaking-react.md
Normal file
@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Breaking React - a common pattern to avoid
|
||||
type: story
|
||||
language: react
|
||||
tags: [debugging]
|
||||
author: chalarangelo
|
||||
cover: broken-screen
|
||||
excerpt: As powerful as React is, it is also quite fragile at places. Did you know that a few lines can easily break your entire React application?
|
||||
dateModified: 2021-11-06T20:51:47+03:00
|
||||
---
|
||||
|
||||
I am by no means an expert React engineer, but I have a couple years of experience under my belt. React is a powerful library for building user interfaces, but it's also quite fragile at places. A common bug I have encountered is caused by **direct DOM manipulation in combination with React**. This is sort of an anti-pattern, as it can break your entire React application under the right circumstances and it's hard to debug.
|
||||
|
||||
Here's [a minimal example](https://codepen.io/chalarangelo/pen/jOEojVJ?editors=0010) of how to reproduce this bug, before we dive into explaining the problem and how to fix it:
|
||||
|
||||
```jsx
|
||||
const destroyElement = () =>
|
||||
document.getElementById('app').removeChild(document.getElementById('my-div'));
|
||||
|
||||
const App = () => {
|
||||
const [elementShown, updateElement] = React.useState(true);
|
||||
|
||||
return (
|
||||
<div id='app'>
|
||||
<button onClick={() => destroyElement()}>
|
||||
Delete element via querySelector
|
||||
</button>
|
||||
<button onClick={() => updateElement(!elementShown)}>
|
||||
Update element and state
|
||||
</button>
|
||||
{ elementShown ? <div id="my-div">I am the element</div> : null }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<App />
|
||||
);
|
||||
```
|
||||
|
||||
This is a pretty simple React application, with a container, two buttons and a state variable. The problem is it will crash if you click the button that calls `destroyElement()` and then click the other one. _Why?_ you might ask. The issue here might not be immediately obvious, but if you look at your browser console you will notice the following exception:
|
||||
|
||||
```
|
||||
Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
|
||||
```
|
||||
|
||||
This might still be cryptic, so let me explain what's going on. React uses its own representation of the DOM, called a **virtual DOM**, in order to figure out what to render. Usually, the virtual DOM will match the current DOM structure and React will process changes in props and state. It will then update the virtual DOM and then batch and send the necessary changes to the real DOM.
|
||||
|
||||
However, in this case React's virtual DOM and the real DOM are different, because of `destroyElement()` removing the `#my-div` element. As a result, when React tries to update the real DOM with the changes from the virtual DOM, the element cannot be removed as it doesn't exist anymore. This results in the above exception being thrown and your application breaking.
|
||||
|
||||
You can refactor `destroyElement()` to be part of the `App` component and interact with its state to fix the issue in this example. Regardless of the simplicity of the problem or the fix, it showcases how fragile React can be under circumstances. This is only compounded in a large codebase where many developers contribute code daily in different areas. In such a setting, issues like this can be easily introduced and tracking them down can be rather tricky. This is why I would advice you to be very careful when directly manipulating the DOM in combination with React.
|
||||
160
snippets/react/s/testing-async-components.md
Normal file
160
snippets/react/s/testing-async-components.md
Normal file
@ -0,0 +1,160 @@
|
||||
---
|
||||
title: Testing React components that update asynchronously with React Testing Library
|
||||
shortTitle: Asynchronous component update testing
|
||||
type: story
|
||||
language: react
|
||||
tags: [testing,event]
|
||||
cover: colorful-lounge
|
||||
excerpt: Testing React components that update asynchronously is pretty common. Learn how to deal with common issues and speed up your testing.
|
||||
dateModified: 2021-11-07T16:34:37+03:00
|
||||
---
|
||||
|
||||
### Components that update asynchronously
|
||||
|
||||
Recently, while working on a side-project, we started using the [React DnD library](https://react-dnd.github.io/react-dnd), as we wanted to implement a multi-container drag and drop system with cards.
|
||||
|
||||
After spending the better part of a day implementing the functionality, we decided to add some tests to ensure everything will keep working as expected. In the aforementioned project, we use [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) to write tests for our components.
|
||||
|
||||
While testing the drag functionality, we came across a very stubborn test. Here's a simplified version of our `Card` component:
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { useDrag } from 'react-dnd';
|
||||
|
||||
const Card = ({
|
||||
card: {
|
||||
id,
|
||||
title
|
||||
}
|
||||
}) => {
|
||||
const [style, drag] = useDrag({
|
||||
item: { id, type: 'card' },
|
||||
collect: monitor => ({
|
||||
opacity: monitor.isDragging() ? 0 : 1
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<li className="card" id={id} ref={drag} style={style}>
|
||||
{title}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
And here's the test we were trying to write originally:
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import Card from './components/Card';
|
||||
// This a little helper we have written to connect to redux and react-dnd
|
||||
import renderDndConnected from './test_utils/renderDndConnected';
|
||||
|
||||
describe('<Card/>', () => {
|
||||
let card;
|
||||
|
||||
beforeEach(() => {
|
||||
const utils = renderDndConnected(
|
||||
<Card card={{ id: '1', title: 'Card' }} />
|
||||
);
|
||||
card = utils.container.querySelector('.card');
|
||||
});
|
||||
|
||||
it('initial opacity is 1', () => {
|
||||
expect(card.style.opacity).toEqual('1');
|
||||
});
|
||||
|
||||
describe('when drag starts', () => {
|
||||
beforeEach(() => {
|
||||
fireEvent.dragStart(card);
|
||||
});
|
||||
|
||||
it('opacity is 0', () => {
|
||||
expect(card.style.opacity).toEqual('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### The dreaded `act(...)` warning
|
||||
|
||||
While the test was obviously not working, the console was constantly nagging about wrapping the test in `act()`:
|
||||
|
||||
```
|
||||
When testing, code that causes React state updates should be wrapped into act(...):
|
||||
|
||||
act(() => {
|
||||
/* fire events that update state */
|
||||
});
|
||||
/* assert on the output */
|
||||
|
||||
This ensures that you're testing the behavior the user would see in the browser.
|
||||
```
|
||||
|
||||
This message wasn't very helpful in identifying the underlying issue. The only thing it highlighted was that the test didn't update the component style immediately. There were pending updates after the test completed. To put it plainly, the test was failing because the `dragStart` event didn't immediately update the `Card` components' style (i.e. set the new `opacity`).
|
||||
|
||||
As a side note, the `Card` component is connected to Redux, which might relate to the issue, but it would most likely happen even without Redux. That's probably due to the fact that `collect` takes some amount of time to run and send an update to the component.
|
||||
|
||||
### Solving the issue
|
||||
|
||||
Digging deeper, we found that apart from `act()`, there are also other options, such as `waitFor()` and `waitForDomChange()`. These seem more intuitive simply because of the name and way they're written (using either `async await` or promises). However, `waitForDomChange()` didn't work properly for our case and our version of `react-testing-library` (which shipped with `react-scripts`) was outdated and did not export `waitFor()`, which took us a good half an hour to figure out.
|
||||
|
||||
After updating `react-testing-library`, we were still not ready to go, as the console started displaying the following error:
|
||||
|
||||
```
|
||||
TypeError: MutationObserver is not a constructor
|
||||
```
|
||||
|
||||
This required some searching, which eventually led us to [this issue](https://github.com/testing-library/react-testing-library/issues/662) which helped us figure out that a solution was to replace the `test` script in our `package.json` with this line:
|
||||
|
||||
```json
|
||||
{
|
||||
// ...
|
||||
"scripts": {
|
||||
"test": "react-scripts test --env=jsdom-fourteen"
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now to finally write a test that works! As mentioned above, we opted to use `waitFor()` from `react-testing-library`, which was actually the only change to the original testing code, except for the dependency bump and the script change described above. Here's the test after making the necessary changes:
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
// This a little helper we have written to connect to redux and react-dnd
|
||||
import renderDndConnected from './test_utils/renderDndConnected';
|
||||
import Card from './components/Card';
|
||||
|
||||
describe('<Card/>', () => {
|
||||
let card;
|
||||
|
||||
beforeEach(() => {
|
||||
const utils = renderDndConnected(
|
||||
<Card card={{ id: '1', title: 'Card' }} />
|
||||
);
|
||||
card = utils.container.querySelector('.card');
|
||||
});
|
||||
|
||||
it('initial opacity is 1', () => {
|
||||
expect(card.style.opacity).toEqual('1');
|
||||
});
|
||||
|
||||
describe('when drag starts', () => {
|
||||
beforeEach(() => {
|
||||
fireEvent.dragStart(card);
|
||||
});
|
||||
|
||||
it('opacity is 0', async() => {
|
||||
await waitFor(() => expect(card.style.opacity).toEqual('0'));
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Summary
|
||||
|
||||
- A message about code that causes React state updates not being wrapped in `act(...)` might indicate that a component updated after the test ended.
|
||||
- Using `waitFor()` can solve the issue by making tests asynchronous, but you might need to bump your `react-testing-library` version if you are using older versions of `react-scripts`.
|
||||
- If you see errors related to `MutationObserver`, you might need to change your `test` script to include `--env=jsdom-fourteen` as a parameter.
|
||||
37
snippets/react/s/testing-portals.md
Normal file
37
snippets/react/s/testing-portals.md
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Testing React portals
|
||||
shortTitle: Portal testing
|
||||
type: story
|
||||
language: react
|
||||
tags: [testing]
|
||||
author: chalarangelo
|
||||
cover: portal-timelapse
|
||||
excerpt: Testing React components that use portals can be difficult until you understand what you really need to be testing.
|
||||
dateModified: 2022-03-13T05:00:00-04:00
|
||||
---
|
||||
|
||||
Testing React components can get pretty complicated, especially when dealing with portals. While they seem intimidating, what they are in essence is a way to render a component in a different place in the DOM. Apart from that, when writing tests, one should avoid testing framework internals. This obviously applies to React internals as well.
|
||||
|
||||
Putting these two points together, all we really care about when testing React portals is if the **portalized output** is correct. Based on that, mocking portals shouldn't be all that hard. We just need to mock `ReactDOM.createPortal()` to render the input element in place. Here's what that looks like in Jest:
|
||||
|
||||
```jsx
|
||||
describe('MyComponent', () => {
|
||||
beforeAll(() => {
|
||||
ReactDOM.createPortal = jest.fn((element, node) => {
|
||||
return element;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ReactDOM.createPortal.mockClear();
|
||||
});
|
||||
|
||||
it('should render correctly', () => {
|
||||
const component = renderer.create(<MyComponent>Hello World!</MyComponent>);
|
||||
|
||||
expect(component.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
This kind of mock will work in most cases and it will help ensure that the portalized output is correct. It comes with some caveats, though. First and foremost, the tested DOM is different from the actual DOM of the application. This can make tests less robust, reducing confidence in their result. Secondly, issues that might arise from the use of portals, such as the portal node missing, must be tested separately.
|
||||
70
snippets/react/s/testing-redux-connected-components.md
Normal file
70
snippets/react/s/testing-redux-connected-components.md
Normal file
@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Testing Redux-connected components with React Testing Library
|
||||
shortTitle: Redux-connected components testing
|
||||
type: story
|
||||
language: react
|
||||
tags: [testing]
|
||||
author: chalarangelo
|
||||
cover: sparkles
|
||||
excerpt: Testing Redux-connected components is pretty common. Learn how to use this simple utility function to speed up your testing.
|
||||
dateModified: 2021-11-07T16:34:37+03:00
|
||||
---
|
||||
|
||||
Testing Redux-connected components with React Testing Library is a very common scenario. However, it might be a little complicated without the proper tools and you could end up repeating yourself. This is especially true when writing the boilerplate to connect to your redux store.
|
||||
|
||||
Here's a simple utility function adapted from [React Testing Library's docs on the subject](https://testing-library.com/docs/example-react-redux) to help you speed up your testing:
|
||||
|
||||
```jsx
|
||||
// src/test/utils/renderConnected.js
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { createStore } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
// Replace this with the appropriate imports for your project
|
||||
import { reducer, reducerInitialState } from './myReduxStore';
|
||||
|
||||
const renderConnected = (
|
||||
ui, {
|
||||
initialState = reducerInitialState,
|
||||
store = createStore(reducer, initialState),
|
||||
...renderOptions
|
||||
} = {}
|
||||
) => {
|
||||
const Wrapper = ({ children }) => (
|
||||
<Provider store={store}>{children}</Provider>
|
||||
);
|
||||
return render(ui, { wrapper: Wrapper, ...renderOptions});
|
||||
};
|
||||
|
||||
export default renderConnected;
|
||||
```
|
||||
|
||||
This utility uses the `createStore` function and the `<Provider>` component to wrap a redux-connected component to the passed state. Then it uses React Testing Library's `render` to finally render the result.
|
||||
|
||||
Remember to replace `import` statements with the appropriate files and exports to set up the utility as necessary. After creating the utility function and saving it in an appropriate file, you can use it like this:
|
||||
|
||||
```jsx
|
||||
// src/test/SomeComponent.test.jsx
|
||||
import React from 'react';
|
||||
// Replace this with the appropriate location of your component
|
||||
import SomeComponent from 'src/components/SomeComponent';
|
||||
// Replace this with the appropriate location of your testing utility
|
||||
import renderConnected from 'src/test/utils/renderConnected';
|
||||
|
||||
describe('<SomeComponent/>', () => {
|
||||
let wrapper, getByText;
|
||||
const initialState = {
|
||||
// ... Add your initial testing state here
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const utils = renderConnected(<SomeComponent />, { initialState });
|
||||
wrapper = utils.container;
|
||||
getByText = utils.getByText;
|
||||
});
|
||||
|
||||
it('renders the component', () => {
|
||||
expect(wrapper.querySelector('.some-component')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
55
snippets/react/s/testing-stateful-ui-components.md
Normal file
55
snippets/react/s/testing-stateful-ui-components.md
Normal file
@ -0,0 +1,55 @@
|
||||
---
|
||||
title: An approach to testing stateful React components
|
||||
shortTitle: Stateful component testing
|
||||
type: story
|
||||
language: react
|
||||
tags: [testing]
|
||||
author: chalarangelo
|
||||
cover: lake-trees
|
||||
excerpt: Testing stateful React components is not difficult, but did you know there's a solution that doesn't involve testing state directly?
|
||||
dateModified: 2021-06-12T19:30:41+03:00
|
||||
---
|
||||
|
||||
Some time ago, I was tasked with writing tests for a handful of React components, an otherwise mundane and uninspiring task, that somehow ended with a "Eureka!" moment for me. The specifics of the project and its components are of little importance, however the key detail is that I was working with stateful React components that are used daily by a large team and, as such, are refactored and updated quite often.
|
||||
|
||||
My initial approach consisted of writing some simple tests, such as checking if the component is rendered properly and if certain events fire appropriately. In doing so, I was comparing state directly with the result I was expecting, having the component's code right next to my assertions. Of course, this isn't bad by anyone's standards, but for a codebase with many moving parts, it is not the greatest idea. Let me show you an example why:
|
||||
|
||||
```js
|
||||
context('the component is initialized in a collapsed state', function() {
|
||||
let wrapper;
|
||||
beforeEach(function(){
|
||||
wrapper = mount(<StatefulComponent />);
|
||||
});
|
||||
|
||||
it('component state.expanded is false', function() {
|
||||
expect(wrapper.state('expanded')).to.be.false;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
In this test, we check if the component's state has `expanded` equal to `false`. Our test will pass, as long as this simple condition is true. It's a very simple test that should be easy to understand even for someone completely unfamiliar with the codebase.
|
||||
|
||||
However, over time the component's implementation might change. What happens if `expanded` in our state ends up meaning something different? Or worse yet, if it isn't reflected the same way in the interface?
|
||||
|
||||
Enter my "Eureka!" moment:
|
||||
|
||||
> The application's UI should always be considered the result of combining the component's props and state.
|
||||
|
||||
The above statement implies that a component's state can be considered a black box while testings, an abstraction layer that should not be accessed unless absolutely necessary. So, instead of the test presented above, we should be doing something more like this:
|
||||
|
||||
```js
|
||||
context('the component is initialized in a collapsed state', function() {
|
||||
let wrapper;
|
||||
beforeEach(function(){
|
||||
wrapper = mount(<StatefulComponent />);
|
||||
});
|
||||
|
||||
it('component does not have the expanded class', function() {
|
||||
expect(wrapper.find('div').hasClass('expanded')).to.be.false;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Our test is still easy to read and understand, but it's a better test in general.
|
||||
|
||||
By directly checking the DOM instead of the component's state, we provide information about the component's output to future code authors, instead of asking them to keep the existing implementation intact. It seems like a better way to document the component and it's easier to track future changes should someone refactor the UI in such a way that the DOM representation of the component is altered.
|
||||
Reference in New Issue
Block a user