React Hooks
Safe Set
Sometimes you will need to only use a useState setter.
Useful if it is possible that your component will unmount after an async function runs but before it finishes.
useEffect(() => {
fetch('http://www.splashbase.co/api/v1/images/random?images_only=true')
.then(response => response.json())
.then(imageObj => safeSetRandomImage(imageObj.url))
}, [someDependancy]);
const mountedRef = useRef(false);
useEffect(() => {
mountedRef.current = true;
return () => (mountedRef.current = false)
}, [someDependancy]);
const safeSetRandomImage = (...args) => mountedRef.current && setRandomImage(...args)
Handle Dynamic Height Changes in Component
See Analytix Security MainContent.js and SearchFilterBar.js
This is a situation where you have a component like a navbar or searchbar and the elements in it can change their height, based on items selected, etc.
To accomplish this, we will use the useEffect hook to setup a listener for resize events and then another useEffect hook to make sure those changes are reflected in our CSS, here being a styled-component. Oh and don't forget we need some state to store the updated height in, thus we will use useState for this.
The updated height (state) in this case will need to be used by the parent component most likely. It will depend on how you have structured your application. In this example, I have:
<MainContent> <SearchBarFilter setSearchBarHeight={setSearchBarHeight} /> <Grid searchBarHeight={searchBarHeight}>{children...}</Grid> </MainContent>
Grid is a styled-component that will use the height to modify it's CSS
useEffect to add Event Listener
The first useEffect hooks set up the window event listener and is only run on mounting. Also note we are sending a cleanup function.
The event listener will only respond to resizes of your window itself. If you have something like a option box that when filled makes the component height larger, you will need to setup an effect to deal with those changes and update the height when that state changes.
// Add an event listener that fires every time window is resized
// Use this so that we can set the 'div' height of SearchFilterBar
useEffect(() => {
window.addEventListener('resize', handleResize.current);
return () => {
window.removeEventListener('resize', handleResize.current);
};
}, []);
handleResize Function
Note that we have a handleResize function that we need to create. I am storing it in a ref, but I don't think that is necessary.
// Stores height of search bar since it dynamically gets bigger/smaller based on
// how many companies are being searched for. Passed up to CSS for appropriate styling
let handleResize = useRef(() => {
setSearchBarHeight(document.getElementById('SearchFilterBar').clientHeight);
});
useEffect To Set The New Height
Whenever the setSearchBarHeight function changes, we run the effect.
useEffect(() => {
setSearchBarHeight(
document.getElementById('SearchFilterBar').clientHeight
);
}, [setSearchBarHeight]);
Style-component using Height
const Grid = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
background: ${baseColors.background};
margin-top: ${props => `${props.searchBarHeight + 10}px`};
position: absolute;
width: 100%;
`;
*Hooks with Context
If you have some state that you will need to be sharing with via context, this seemed like a great way to "package" the state, state setters and context all together.
Main ideas for this came from article by Kent Dodds
The idea is that we have a single file that exports the following:
- StateProvider - This is the context provider and will be the default export from the file.
- use...State - This is the hook that will return the state values to you. This keeps your files consuming the context from having to use the useContext hook and also they will not have to import the context
- use...StateSetters - This is the hook that returns the state setters.
I stored this file in a folder called context with the thought that if you have multiple sets of data that will be shared at different levels of the application, we will have different contexts.
This example is for storing information on viewing/editing a Qlikview Variable, hence all of the naming have Variable in them.
The first piece of the file creates our needed Contexts, one to hold the State, the other to hold the Setters.
Then we build the function that will return our Context Provider.
Note that all our state is created inside this provider function.
// variableStateContext.js
import React, { useState, useContext } from "react";
const VariableStateContext = React.createContext();
const VariableSettersContext = React.createContext();
/**======================================================
* Provider function
* This function is also where all the state is created
*/
function VariableStateProvider({ children }) {
let [viewingId, setViewingIdMain] = useState();
let [isEditing, setIsEditing] = useState(false);
let [isDirty, setIsDirty] = useState(false);
// When setting the viewing ID need to take into account state
// of being edited
let setViewingId = newViewingId => {
if (newViewingId) {
setIsEditing(false);
}
setViewingIdMain(newViewingId);
};
return (
<VariableStateContext.Provider value={{ viewingId, isEditing, isDirty }}>
<VariableSettersContext.Provider
value={{ setViewingId, setIsEditing, setIsDirty }}
>
{children}
</VariableSettersContext.Provider>
</VariableStateContext.Provider>
);
}
export const useVariableState = () => { ... }
export const useVariableStateSetters = () => { ... }
export default VariableStateProvider;
Now that we have the provider, we will build out our hooks that will get the context for us.
/**======================================================
* Variable State
*
* useVariableState()
* Returns and object with the Variable state
* { viewingId, isEditing, isDirty }
*
* Just a helper hook so that the user doesn't
* need to import the VariableStateContext and useContext
*/
export const useVariableState = () => {
const context = useContext(VariableStateContext);
if (context === undefined) {
throw new Error(
"useVariableState must be used within a VariableStateProvider"
);
}
return context;
};
/**======================================================
* Variable State Setters
*
* useVariableStateSetting()
* Return an object with the setter functions
* { setViewingId, setIsEditing, setIsDirty }
*
*
*/
export const variableStateContext = () => {
const context = useContext(VariableSettersContext);
if (context === undefined) {
throw new Error(
"useVariableStateSetters must be used within a VariableStateProvider"
);
}
return context;
};
Straight forward, read Kent's article for the details.
Lastly, to implement, you will need to first place the provider around the components that need access to the state.
import VariableStateProvider from '../context/variableStateContext';
...
<VariableStateProvider>
<VariableMain />
</VariableStateProvider>
...
To access the context, just use the hooks!
import { useVariableState, useVariableStateSetters} from '../context/variableStateContext';
function VariableMain() {
let { viewingId, isEditing, isDirty } = useVariableState;
let { setViewingId, setIsEditing, setIsDirty } = useVariableStateSetters;
...
}
Another Context Option with useReducer
This is another way to use context with the useReducer hook.
It is a function that exports your Context and Provider component. It is a Context factory that you can use to setup as many Contexts needed.
// createDataContext.js
import React, { useReducer } from "react";
export default (reducer, actions, initialState) => {
const Context = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
let boundActions = Object.keys(actions).reduce((ba, key) => {
ba[key] = actions[key](dispatch);
return ba;
}, {});
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
};
To use this function you would create your context file. You will need a reducer function, an actions object and initial state.
If you need async actions in your reducer, you will need to update the dispatch function to be able to deal with them.
// BlogContext.js
import createDataContext from "./createDataContext";
const blogReducer = (state, action) => {
switch (action.type) {
case "add_blogpost":
return [...state, { title: `Blog Post #${state.length + 1}` }];
default:
return state;
}
};
const addBlogPost = dispatch => {
return () => {
dispatch({ type: "add_blogpost" });
};
};
export const { Context, Provider } = createDataContext(
blogReducer,
{ addBlogPost },
[]
);
Lastly, you will need to import your BlogContext into the files that need the data. Also, don't forget to wrap your component tree in the Provider component.
Make Reducer Handle Thunks
I originally coded this to handle multiple reducers, however, I think that having separate contexts for each reducer may be better. Here is the multiple reducer implementation:
/**
* thunk dispatch
* @param {array} dispatchArr - Array of dispatch functions returned from useReducer calls
* @param {any} store - main store that holds all data from above mentioned useReducer calls
*
* @returns {function} - returns a function that accepts an action.
* The action can be a standard redux style action {type: '', payload: ''}
* or a function, i.e. thunk
* If it is a function/thunk/async func then we will call it and pass the "fullDispatch" function
* along with the store.
*/
function useThunkDispatch(dispatchArr, store) {
return action => {
let fullDispatch = action => {
dispatchArr.forEach(dispatchFunc => {
dispatchFunc(action);
});
};
if (typeof action === 'function') {
action(fullDispatch, store);
} else {
fullDispatch(action);
}
};
}
Here is the implementation without the multiple reducers
/**
* thunk dispatch
* @param {function} dispatch - dispatch function returned from useReducer call
* @param {any} store - store that holds the data(state)
*
* @returns {function} - returns a function that accepts an action.
* The action can be a standard redux style action {type: '', payload: ''}
* or a function, i.e. thunk
* If it is a function/thunk/async func then we will call it and pass the "fullDispatch" function
* along with the store.
*/
function useThunkDispatch(dispatch, store) {
return action => {
let fullDispatch = action => {
dispatch(action);
};
if (typeof action === 'function') {
action(fullDispatch, store);
} else {
fullDispatch(action);
}
};
}
When using the above, you may want to wrap the dispatch in a useRef so that it doesn't get recreated each time.
Search Input Box Hook
Originally implemented as a Render Prop component, this is the hook implementation.
To see the Render Prop implementation, go to Search Input Box Component
The idea with this hooks is that you will pass it the searchArray that will be used to match with typed in input. The hook returns the following:
{
searchingIsOn, // boolean letting us know if the search function is active
spreadProps // props that need to be spread on the input control
}
spreadProps is itself an object the contains
spreadProps: {
ref,
value,
onKeyDown,
onChange
}
Since the updated value is always being returned in spreadProps, you can pull it out early on and use it as needed:
import React from "react";
import useSearchInput from "./useSearchInput";
const authors = [
{ author: "mark" },
{ author: "lori" },
{ author: "Haley" },
{ author: "Hunter" }
];
function App() {
let [author, setAuthor] = React.useState("");
let {searchingIsOn, spreadProps} = useSearchInput({
searchArray: authors.map(author => author.author)
});
let { value } = spreadProps;
return (
<div className="App">
<input
style={searchingIsOn ? { backgroundColor: 'white'} : {backgroundColor: 'yellow'}}
{...spreadProps} placeholder="Use an Author" />
</div>
);
Code for useSearchInput hook
import { useState, useRef, useEffect } from "react";
/**
* parameters: { searchArray: [] }
* Uses the passed searchArray and display like google predictive search
* Escape key press turns off searching, press again, turns back on
* the "searchingIsOn" value returned from useSearchInput() lets you know if
* searching is on or off
*
* @param {Object} - { searchArray: [] }
* @example
* let {searchingIsOn, spreadProps} = useSearchInput({
* searchArray: arrayToSearch
* });
* // get the value so you can use it after it is input
* let { value } = spreadProps;
* ....
* <input {...spreadProps}
* style={searchingIsOn ? { backgroundColor: 'white'} : {backgroundColor: 'yellow'}}
* placeholder="Input a Value"
* />
*/
const useSearchInput = ({ searchArray }) => {
const [inputValue, setInputValue] = useState("");
const [backspace, setBackspace] = useState(false);
const [escKey, setEscKey] = useState(false);
const [startPos, setStartPos] = useState(0);
const [endPos, setEndPos] = useState(0);
const inputEl = useRef();
const onKeyDown = e => {
const keyPressed = e.key;
//Check keyPressed and set selection
switch (keyPressed) {
case "ArrowRight":
setStartPos(inputEl.current.selectionStart + 1);
setEndPos(inputEl.current.selectionStart + 1);
break;
case "ArrowLeft":
setStartPos(inputEl.current.selectionStart - 1);
setEndPos(inputEl.current.selectionStart - 1);
break;
case "Backspace":
if (startPos !== endPos) {
setStartPos(inputEl.current.selectionStart);
setEndPos(inputEl.current.selectionStart);
} else {
setStartPos(inputEl.current.selectionStart - 1);
setEndPos(inputEl.current.selectionStart - 1);
}
setBackspace(true);
break;
case "Delete":
setStartPos(inputEl.current.selectionStart);
setEndPos(inputEl.current.selectionStart);
setBackspace(true);
break;
case "Escape":
setStartPos(inputEl.current.selectionStart);
setEndPos(inputEl.current.selectionStart);
setEscKey(!escKey);
break;
default:
break;
}
};
const onInputChange = e => {
//Get input value
//CHECK FOR this.state.backspace and if true, set state to target.value passed
// and set backspace to false
const inputValue = e.target.value;
if (backspace || escKey) {
setInputValue(inputValue);
setBackspace(false);
return;
}
//Setup match expression
const matchExpr = inputValue.length > 0 ? "^" + inputValue : /.^/;
//Create RegExp Object
const expr = new RegExp(matchExpr, "ig");
//Try and Find a match in array of service inputValues
const foundItem = searchArray.find(desc => desc.match(expr));
// console.log(`foundItem ${foundItem}`);
//If not found, return inputValue, else return found item and set selection range
const finalValue = foundItem || inputValue;
setStartPos(inputValue.length);
setEndPos(finalValue.length);
// console.log(`startpos: ${startPos} -- endpos: ${endPos} -- foundItem: ${foundItem}`)
setInputValue(finalValue);
};
useEffect(() => {
if (startPos !== endPos) {
inputEl.current.setSelectionRange(startPos, endPos);
}
});
return {
searchingIsOn: !escKey,
spreadProps: {
ref: inputEl,
value: inputValue,
onKeyDown: onKeyDown,
onChange: onInputChange
}
};
};
export default useSearchInput;
UseOnKeyPress Hook
const useOnKeyPress = (targetKey, onKeyDown, onKeyUp, isDebugging = false) => {
const [isKeyDown, setIsKeyDown] = useState(false);
const onKeyDownLocal = useCallback(e => {
if (isDebugging)
console.log(
"key down",
e.key,
e.key != targetKey ? "- isn't triggered" : "- is triggered"
);
if (e.key != targetKey) return;
setIsKeyDown(true);
if (typeof onKeyDown != "function") return;
onKeyDown(e);
});
const onKeyUpLocal = useCallback(e => {
if (isDebugging)
console.log(
"key up",
e.key,
e.key != targetKey ? "- isn't triggered" : "- is triggered"
);
if (e.key != targetKey) return;
setIsKeyDown(false);
if (typeof onKeyUp != "function") return;
onKeyUp(e);
});
useEffect(() => {
addEventListener("keydown", onKeyDownLocal);
addEventListener("keyup", onKeyUpLocal);
return () => {
removeEventListener("keydown", onKeyDownLocal);
removeEventListener("keyup", onKeyUpLocal);
};
}, []);
return isKeyDown;
};
Publish Hook to NPM
The create-react-hook module sets up a project so that you can deploy your react hook.
$ npx create-react-hook
? Package Name: @yournpmusename/useMyNewHook // scopes hook to your user
? Package Description: a description for the hook
? Authors GitHub Handle: markmccoid
? Github Repo Path: markmccoid\useMyNewHookd
...
Add your hook code to the index.js
in the src
directory. Egghead course
docs)
npm version (You can use the command line tool npm version ...
to bump your version. It will also set a git tag for the version.