I know that the whole point of react hooks are to get away from class based component and promote functional component. However, is it possible to achieve inheritance in react hooks?
For example here, I create two hooks.
1. useEmailFormatValidator of which validates if input (onChange) is in valid Email format.
2. useEmailSignupValidator inherits useEmailFormatValidator but extends it's capability to verify user can use the username when (onBlur) event happens.
The whole point of these are useEmailFormatValidator can be used in log-in form where as useEmailSignupValidator can be used in sign-up form.
Below is my code
useEmailFormatValidator
import { useState } from 'react';
export const useEmailFormatValidator = () => {
const [value, setValue] = useState('');
const [validity, setValidity] = useState(false);
const regex = /^(([^<>()[\]\\.,;:\s#"]+(\.[^<>()[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const inputChangeHandler = event => {
setValue(event.target.value.trim());
if (regex.test(String(event.target.value.trim()).toLowerCase())) {
setValidity(true);
} else {
setValidity(false);
}
};
return { value: value, onChange: inputChangeHandler, validity };
};
useEmailSignupValidator
import { useState } from 'react';
import { useEmailFormatValidator } from "../auth";
export const useEmailSignupValidator = () => {
const [value, setValue] = useState('');
const [validity, setValidity] = useState(false);
const emailFormatValidator = useEmailFormatValidator();
const inputChangeHandler = event => {
emailFormatValidator.onChange(event);
setValue(emailFormatValidator.value);
setValidity(emailFormatValidator.validity);
};
const verifyUserNameExists = event => {
// Verify username is availble in back-end.
};
return { value: value, onChange: inputChangeHandler, onBlur: verifyUserNameExists, validity };
};
When I run a test, below does not work as expected and 'value' and 'validity' is undefined.
const inputChangeHandler = event => {
emailFormatValidator.onChange(event);
setValue(emailFormatValidator.value);
setValidity(emailFormatValidator.validity);
};
Is there anyway to have inheritance in custom hooks? Or how do you re-use code, of which is the purpose of react hooks?
This isn't really a custom hooks thing, imho. Why not do something like this?
const isValidEmail = email => {
const regex = /^(([^<>()[\]\\.,;:\s#"]+(\.[^<>()[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return regex.test(email);
};
const MyComponent = () => {
const [inputVal, setInputVal] = useState('');
return(
<div>
<input
type='text'
name='my-input'
onChange={e => {
const email = e.target.value;
if(isValidEmail(email)){
setInputVal(email);
}else{
alert('invalid email')
}
}}
/>
</div>
)
}
Related
I'm doing a social networking project on React
I wanted to replace one component from class - to functional and use hooks, and a global problem appeared:
When I go to a new user, the page displays the status of the previous one
I use useState() hook, debugged everything, but for some reason when a new status component is rendered, it doesn't update
const ProfileStatus = (props) => {
const [edditMode, setEdditMode] = useState(false);
const [status, setValue] = useState(props.status || "Empty");
const onInputChange = (e) => {
setValue(e.target.value);
};
const activateMode = () => {
setEdditMode(true);
};
const deactivateMode = () => {
setEdditMode(false);
props.updateUserStatus(status);
};
I thought the problem was that the container component was still a class component, but by redoing it, nothing has changed
One way to solve this is by using the useEffect hook to trigger an update when props change. You can use the hook to do comparison between current props and previous props, then update status in the state.
Use this as reference and adapt according to your own code.
const ProfileStatus = (props) => {
const [edditMode, setEdditMode] = useState(false);
const [status, setValue] = useState(props.status || "Empty");
useEffect(() => {
setValue(props.status || "Empty");
}, [props.status]);
const onInputChange = (e) => {
setValue(e.target.value);
};
const activateMode = () => {
setEdditMode(true);
};
const deactivateMode = () => {
setEdditMode(false);
props.updateUserStatus(status);
};
I juste created a custom hook to update a value from a calendar picker, and getting it updated in a Form component.
export const useCalendar = (props) => {
const [startDate, setStartDate] = useState("");
useEffect(() => {
console.log("startDate 2", startDate);
}, [startDate]);
return {startDate, setStartDate}
};
export const CalendarPicker = () => {
const { startDate, setStartDate } = useCalendar();
const handleDayPress = (newDateObject) => {
setStartDate(newDateObject);
}
};
export const Form = () => {
const { startDate } = useCalendar();
useEffect(() => {
console.log("startDate", startDate);
}, [startDate]);
}
The value is getting updated in the custom Hook but not in the Form component
Getting the value updated from the custom Hook to the Form
I have class component functions that handle a search function.
filterData(offers,searchKey){
const result = offers.filter((offer) =>
offer.value.toLowerCase().includes(searchKey)||
offer.expiryDate.toLowerCase().includes(searchKey)||
offer.quantity.toLowerCase().includes(searchKey)
)
this.setState({offers:result})
}
const handleSearchArea = (e) =>{
const searchKey=e.currentTarget.value;
axios.get(`/viewPendingSellerOffers`).then(res=>{
if(res.data.success){
this.filterData(res.data.existingOffers,searchKey)
}
});
}
Now I try to convert these class component functions to functional component functions. To do this I tried this way.
const filterData = (offers,searchKey) => {
const result = offers.filter((offer) =>
offer.value.toLowerCase().includes(searchKey)||
offer.expiryDate.toLowerCase().includes(searchKey)||
offer.quantity.toLowerCase().includes(searchKey)
)
setState({offers:result})
}
const handleSearchArea = (e) =>{
const searchKey=e.currentTarget.value;
axios.get(`/viewPendingSellerOffers`).then(res=>{
if(res.data.success){
filterData(res.data.existingOffers,searchKey)
}
});
}
But I get an error that says "'setState' is not defined". How do I solve this issue?
Solution:
import React, {useState} from "react";
import axios from "axios";
const YourFunctionalComponent = (props) => {
const [offers, setOffers] = useState()
const filterData = (offersPara, searchKey) => {// I changed the params from offers to offersPara because our state called offers
const result = offersPara.filter(
(offer) =>
offer?.value.toLowerCase().includes(searchKey) ||
offer?.expiryDate.toLowerCase().includes(searchKey) ||
offer?.quantity.toLowerCase().includes(searchKey)
);
setOffers(result);
};
const handleSearchArea = (e) => {
const searchKey = e.currentTarget.value;
axios.get(`/viewPendingSellerOffers`).then((res) => {
if (res?.data?.success) {
filterData(res?.data?.existingOffers, searchKey);
}
});
};
return (
//To use your *offers* state object just call it like this {offers?.El1?.El2}
);
};
export default YourFunctionalComponent;
Note: It is recommended to do null check before accessing nested objects like this res?.data?.existingOffers, Optional chaining will help us in this regard.
I'm new doing automation testing, so I'm trying to do some unit tests, I built a usual custom hook to handle the form events and the inputs changes.
import { useState } from 'react'
const useFormHook = (callback) => {
const [inputs, setInputs] = useState({});
const handleSubmit = (e) => {
if (e) {
e.preventDefault();
}
setInputs({})
callback();
}
const handleInputChange = (e) => {
e.persist();
setInputs(inputs => ( {...inputs, [e.target.name]: e.target.value } ));
}
return{
handleSubmit,
handleInputChange,
inputs
}
}
export default useFormHook
and the implementation goes in to a search bar, which event will update a context state, and looks like this:
const SearchBar = () => {
const [search, setSearch] = useState('')
useAsyncHook(search); // I'm using an async custom hook to fetch some data with axios.
const handleFormSubmit = () => {
let targetValue = Object.values(inputs).join().toLowerCase()
setSearch(targetValue)
document.forms[0].reset()
}
const { inputs, handleSubmit, handleInputChange } = useCustomFormHook(handleFormSubmit)
return (
<form onSubmit={handleSubmit} className="searchForm">
<input
id='searchBar'
type="text"
name="value"
value={inputs.value || ''}
onChange={handleInputChange}
required
className={`input`}
/>
<input
className={`button primary`}
type="submit"
value="🔍 Search"
id='search-button'
/>
</form>
)
}
till now this work fine..:
import React from "react";
import { mount, shallow } from "enzyme";
//components and hooks
import useFormHook from "../../../Services/Hooks/customFormHook";
//import useAsyncHook from "../../../Services/Hooks/customAsyncHook";
//import SeachBar from "../SearchBar";
describe("Custom hooks suite test", () => {
// const wrapper = shallow(<SeachBar />);
let results;
const handleHookTester = (hook) => {
function HookWrapper() {
results = hook();
return null;
}
mount(<HookWrapper />);
return results;
};
it("Form hook test", () => {
handleHookTester(useFormHook);
expect(results.inputs).toStrictEqual({});
});
});
So far I've managed to make it work, but this is just the customFormHook and I'd like to test and my customAsyncHook which looks like this:
const useAsyncHook = (id) => {
const [loading, setLoading] = useState('false');
const { findData } = useContext(AppContext)
// 1 -
useEffect(() => {
async function getData() {
try {
setLoading('true');
const response = await axios(
`someEndPoint/${id}`
);
findData({type:FIND_DATA, data:response.data });
} catch (error) {
setLoading('null');
findData({type:FIND_DATA, data:null });
}
}
if (id !== "") {
getData();
}
}, [id]);
return loading;
}
and if I try something like const wrapper = shallow(<SearchBar/>) or:
handleHookTester(useAsyncHook); the test fails throwing the following error:
● Custom hooks suite test › encountered a declaration exception
TypeError: Cannot destructure property 'findData' of '(0 , _react.useContext)(...)' as it is undefined.
8 | const [loading, setLoading] = useState('false');
9 |
> 10 | const { findData } = useContext(AppContext)
| ^
11 | // 1 -
12 | useEffect(() => {
So the question is should I mock that function? and how?
I nav component then will toggle state in a sidebar as well as open and close a menu and then trying to get this pass in code coverage. When I log inside my test my state keeps showing up as undefined. Not sure how to tackle this one here.
Component.js:
const Navigation = (props) => {
const {
classes,
...navProps
} = props;
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return (
<Fragment>
<Button
onClick={toggleMenuOpen}
/>
<SideMenu
toggleSidebar={toggleSidebar}
>
<Menu
onClose={toggleMenuClose}
>
</SideMenu>
</Fragment>
);
};
export default Navigation;
Test.js:
import { renderHook, act } from '#testing-library/react-hooks';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => Navigation({ ...newProps }));
expect(result.current.sidebarOpen).toBeFalsy();
});
Author of react-hooks-testing-library here.
react-hooks-testing-library is not for testing components and interrogating the internal hook state to assert their values, but rather for testing custom react hooks and interacting withe the result of your hook to ensure it behaves how you expect. For example, if you wanted to extract a useMenuToggle hook that looked something like:
export function useMenuToggle() {
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return {
sidebarOpen,
toggleSidebar,
toggleMenuClose,
toggleMenuOpen
}
}
Then you could test it with renderHook:
import { renderHook, act } from '#testing-library/react-hooks';
// Hooks
import { useMenuToggle } from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => useMenuToggle());
expect(result.current.sidebarOpen).toBeFalsy();
act(() => {
result.current.toggleSidebar()
})
expect(result.current.sidebarOpen).toBeTruthy();
});
Generally though, when a hook is only used by a single component and/or in a single context, we recommend you simply test the component and allow the hook to be tested through it.
For testing your Navigation component, you should take a look at react-testing-library instead.
import React from 'react';
import { render } from '#testing-library/react';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { getByText } = render(<Navigation {...newProps} />);
// the rest of the test
});