Custom Hooks - React
export MyComponent = ({children}) => (
<div className="MyComponent">
{children}
</div>
),
export MyComponent = ({text}) => {
const [message, setMessage] = useState('');
useEffect(() => {
setMessage(text === 'Marco'
? 'Polo'
: children);
}, [text]);
return (
<div className="MyComponent">
{children}
</div>
)
}
export const useMessage = (text) => {
const [message, setMessage] = useState('');
useEffect(() => {
setMessage(text === 'Marco'
? 'Polo'
: children);
}, [text]);
return { message }
}
... but not at all
export const useViewport = () => {
const [viewport, setViewport] = useState(null);
const handleResize = () => {
if (window.innerWidth > 1200) {
setViewport('extra-large')
} else if (window.innerWidth > 992) {
setViewport('large')
} else if (window.innerWidth > 768) {
setViewport('medium')
} else if (window.innerWidth > 576) {
setViewport('small')
} else {
setViewport('extra-small')
}
};
useEffect(() => {
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
})
return { viewport };
}
describe('useViewport', () => {
it('changes the viewport', () => {
});
});
describe('useViewport', () => {
const TestComponent = () => {
const { viewport } = useViewport();
return <span>{viewport}</span>
}
const fireResizeEvent = (width) => {
window.innerWidth = width
window.dispatchEvent(new Event('resize'))
}
it('changes the viewport to medium', () => {
const expectedViewport = 'medium';
const view = render(<TestComponent />);
fireResizeEvent(800)
view.rerender();
expect(
view.queryByText(expectedViewport)
).toBeInDocument();
});
});
Create an specific component
Select by text in DOM
Test our hook depends on component
Each mutation should be handled outside
Tools:
describe('useLocale', () => {
it('changes the state', async () => {
const newState = 'New state';
const { result } = renderHook(() => useHook('Initial state'));
await act(async () => {
await result.current.setState('irrelevant state');
});
expect(result.current.state).toEqual(newState);
});
});
We are not exposing anything
export const useViewport = () => {
const [viewport, setViewport] = useState(null);
const handleResize = () => {
if (window.innerWidth > 1200) {
setViewport('extra-large')
} else if (window.innerWidth > 992) {
setViewport('large')
} else if (window.innerWidth > 768) {
setViewport('medium')
} else if (window.innerWidth > 576) {
setViewport('small')
} else {
setViewport('extra-small')
}
};
useEffect(() => {
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
})
return viewport;
}
export const useViewport = () => {
const [viewport, setViewport] = useState(null);
const resizedTo = (width) => {
if (width > 1200) {
setViewport('extra-large')
} else if (width > 992) {
setViewport('large')
} else if (width > 768) {
setViewport('medium')
} else if (width > 576) {
setViewport('small')
} else {
setViewport('extra-small')
}
};
return {viewport, resizedTo};
}
describe('useViewport', () => {
it('changes the viewport', async () => {
const { result } = renderHook(() => useViewport());
act(() => {
result.current.resizedTo(800)
});
expect(result.current.viewport).toEqual('medium');
});
});
const MyComponent = () => {
const {viewport, resizedTo} = useViewport();
const handleResize = () => { resizedTo(window.innerWidth) };
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
}
})
return (<>{// irrelevant code}</>)
}
This is not for test, is for define functionalities and think about the code
export useProducts = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
// remember to expose your methods
const getProducts = async () => {
setLoading(true);
try {
setProducts(await productRepository.getProducts);
} catch(error) {
setErrorMessage(error.message)
} finally {
setLoading(false);
}
}
return { products, getProducts, loading, error }
}
export useProducts = (repository = productRepository) => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
// remember to expose your methods
const getProducts = async () => {
setLoading(true);
try {
setProducts(await repository.getProducts);
} catch(error) {
setErrorMessage(error.message)
} finally {
setLoading(false);
}
}
return { products, getProducts, loading, error }
}
describe('useAsyncProduct', () => {
it('retrieves products from repository', async () => {
const product = buildProduct({});
const productRepositoryMock = {
getProducts: jest.fn(() => Promise.resolve([product])),
};
const { result } = renderHook(() => useProducts(productRepositoryMock));
await act(async () => {
await result.current.getProducts();
});
expect(result.current.products).toEqual([products]);
expect(result.current.loading).toBeFalsy();
});
});