I know that we can create multiple custom hooks in separate files for example
useCounter.js and useToggle.js.
My Question
Can we create a Javascript class with its methods as custom hooks and use it in our functional components. Is this an anti-pattern ? The reason I am thinking to put it in a JS class is so that I don't have to create multiple files for custom hook.
Something Like below
class CustomHooks {
useCounter() {
const [count, setCount] = React.useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return { count, increment, decrement };
}
useToggle(initialState = false) {
const [state, setState] = React.useState(initialState);
const toggle = () => setState(!state);
return { state, toggle };
}
}
And use it like below
const hooks = new CustomHooks();
const Counter = () => {
const { count, increment, decrement } = hooks.useCounter();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};
const Toggler = () => {
const { state, toggle } = hooks.useToggle();
return (
<div>
<p>Toggler is {state ? "on" : "off"}</p>
<button onClick={toggle}>Toggle</button>
</div>
);
};
const App = () => {
return (
<div>
<Counter />
<Toggler />
</div>
);
};
You cannot use React hooks in class. If your goal is not to have multiple files for multiple hooks, create a single file and export multiple hooks from it.
// hooks.js
export const useToggle = () => {
// hook implementation
}
export const useCounter = () => {
// hook implementation
}
And use them in your components
// App.js
import { useToggle, useCounter } from './hooks';
function App() {
const toggle = useToggle();
const counter = useCounter();
return <div>hi there</div>
}
You can simply create an object that will hold the hooks.
customhooks.js
function useCounter () {
...
}
function useToggle(initialState = false) {
...
}
export const hooks = {
useCounter,
useToggle
}
counter.js
import { hooks } from "./customhooks.js";
const Counter = () => {
const { count, increment, decrement } = hooks.useCounter();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};
However, personally I recommend handling with individual files per hook. Especially when the project gets bigger and we have many hooks, we eventually get to the point that we need different hooks in different areas and it can be detrimental to performance if all hooks have to be loaded at once. Of course, this is not only related to the number but also to the complexity of the hooks and what dependencies they have in turn.
Related
I am making a chess game and I need to integrate a count down timer, e.g - so when it's the white players turn their clock is counting down until they move.
To avoid changing a lot more code that is not present in this question ,I would like to keep function "move" as is, though I could change CountDownTimer if needed,
For now I would like to know how to call the react hook setTimerOn in the CountDownTimer component from the function move,then I can work out the rest myself.
From what I have read you cannot do this with react hooks? so just looking for
anything that works, if that means changing CountDownTimer.
export function move(from, to, promotion) {
let tempMove = { from, to }
if (member.piece === chess.turn()) {
const legalMove = chess.move(tempMove)
if (legalMove) {
updateGame()
//i would like to access CountDownTimer and call setTimerOn(true)
// i can work out the code for white and black / start and stop myself afterwards
}
}
,
}
import React from 'react'
function CountDownTimer(){
const [time, setTime] = React.useState(0);
const [timerOn, setTimerOn] = React.useState(false);
React.useEffect(() => {
let interval = null;
if (timerOn) {
interval = setInterval(() => {
setTime((prevTime) => prevTime + 10);
}, 10);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn]);
return (
<div className="Timers">
<h2>Stopwatch</h2>
<div id="display">
<span>{("0" + Math.floor((time / 60000) % 60)).slice(-2)}:</span>
<span>{("0" + Math.floor((time / 1000) % 60)).slice(-2)}:</span>
<span>{("0" + ((time / 10) % 100)).slice(-2)}</span>
</div>
<div id="buttons">
{!timerOn && time === 0 && (
<button onClick={() => setTimerOn(true)}>Start</button>
)}
{timerOn && <button onClick={() => setTimerOn(false)}>Stop</button>}
{!timerOn && time > 0 && (
<button onClick={() => setTime(0)}>Reset</button>
)}
{!timerOn && time > 0 && (
<button onClick={() => setTimerOn(true)}>Resume</button>
)}
</div>
</div>
);
});
export default CountDownTimer; ```
Based on React documentation, React Hook can be called from function components or other hooks.
In your situation, you should consider the utilization of React Context. You need to move up timerOn state and setTimerOn method as context values. So that, all components which are wrapped by the context provider can utilize the values.
First, create some helpers for managing context.
// TimerOn.js
import React, { createContext, useContext } from 'react';
// create a context
const TimerOnContext = createContext();
// create a hook to provide context value
export function useTimerOn() {
const contextValue = useContext(TimerOnContext);
return contextValue;
}
// custom provider that will wrap your main components
export function TimerOnProvider({ children }) {
const [timerOn, setTimerOn] = React.useState(false);
return (
<TimerOnContext.Provider value={{ timerOn, setTimerOn }}>
{children}
</TimerOnContext.Provider>
);
}
For instance, I create two simple components to demonstrate the timer component and caller component.
// CountDownTimer.js
import React from "react";
import { useTimerOn } from "./TimerOn";
export default function CountDownTimer() {
const { timerOn } = useTimerOn();
// detect changes
React.useEffect(() => {
if (timerOn) {
console.log('timer is on');
} else {
console.log('timer is off');
}
}, [timerOn]);
return (
<div>{timerOn ? 'timer on' : 'timer off'}</div>
);
}
// MoveCaller.js
import React from "react";
import { useTimerOn } from "./TimerOn";
export default function MoveCaller() {
const { timerOn, setTimerOn } = useTimerOn();
// move then set timer
const move = () => {
setTimerOn(!timerOn);
}
return (
<div>
<button type="button" onClick={move}>
Move
</button>
</div>
);
}
Then, you can wrap all main components with the provider component. So, the move function in a component can change the state of timerOn and be read by another component.
import React from 'react';
import CountDownTimer from './CountDownTimer';
import MoveCaller from './MoveCaller';
import { TimerOnProvider } from './TimerOn';
export default function ChessApp() {
return (
<TimerOnProvider>
<CountDownTimer />
<MoveCaller />
</TimerOnProvider>
);
}
I am new to React and just testing some simple helper functions. Are there any drawbacks to passing a state setter to a helper function? This is just an oversimplified example:
App.js
import { useState } from 'react';
import increment from './helpers/increment';
import reset from './helpers/reset';
const App = () => {
const [counter, setCounter] = useState(0)
const [even, setEven] = useState(true)
return (
<div>
<div>{counter}</div>
<div>
<button onClick={() => increment(setCounter, setEven)}>INCREMENT</button>
<button onClick={() => reset(setCounter, setEven)}>RESET</button>
</div>
<div>Number is even: {even.toString()}</div>
</div>
);
}
export default App;
increment.js
const increment = (setCounter, setEven) => {
setCounter(current => {
setEven((current + 1) % 2 === 0 ? true : false)
return current + 1
})
}
export default increment;
reset.js
const reset = (setCounter, setEven) => {
setCounter(0)
setEven(true)
}
export default reset;
UPDATE:
Looks like a custom hook was what I needed in this case. (Thanks #Houssam)
useCounter.js
import { useState } from "react";
const useCounter = () => {
const [counter, setCounter] = useState(0)
const [even, setEven] = useState(true)
const increment = () => {
setCounter(current => {
setEven((current + 1) % 2 === 0 ? true : false)
return current + 1
})
}
const reset = () => {
setCounter(0)
setEven(true)
}
return { counter, even, increment, reset }
}
export default useCounter
There is nothing wrong with passing state update functions to helper functions as you are simply refactoring your code.
A common pattern is to define those helper functions inside your component, but if you do need to use them in other components, then defining them as helper functions is the right thing to do.
Another solution to your problem is to define a custom hook that encapsulates both of the counter and even state and returns the increment and reset functions. You can learn more about custom hooks on the documentation.
If you define a custom hook, the goal will be to be able to do something like:
const [counter, even, increment, reset] = useCounter(0);
I am creating a web app, which is basically an image gallery for a browser game.
The avatars are stored in the game in this format:
https://websitelink.com/avatar/1
https://websitelink.com/avatar/2
https://websitelink.com/avatar/3
So i want to build 2 navigation buttons, one will increment the counter, to move to next image and another one will decrement the counter to move to previous image.
I tried to use props, but since props are immutable it didn't work.
How do I approach building this web app?
Here is the minimal code which may help you to understand about the React Component, props and state.
// parent compoment
import { useState } from "react"
export const GameImageGallery = () => {
const [num, setNum] = useState(0)
const increaseDecrease = (state) => {
if (state === "+") {
setNum(num + 1)
}
if (state === "-") {
setNum(num - 1)
}
}
return (
<>
<button onClick={() => increaseDecrease("-")}>--</button>
<button onClick={() => increaseDecrease("+")}>++</button>
<Image url={`https://websitelink.com/avatar/${num}`} />
</>
)
}
// child component to show image
const Image = ({ url }) => {
return <img src={url} alt="image" />
}
you can do this thing,
const [id,setId]=useState(0);
useEffect(() => {
},[id])
const increment = () => {
setId(id++);
}
const decrement = () => {
setId(id--);
}
return(
<button onClick={increment}>Add</button>
<button onClick={decrement}>remove</button>
<img url={`https://websitelink.com/avatar/${id}`} />
)
useRef is ideal to manage data persistently in a component.
Example:
import { useRef } from 'react'
...
const App = () => {
const links = useRef({url1Ctr : 1})
const onBtnClick = () => {
links.current = { url1Ctr: links.current.url1Ctr + 1}
}
...
}
I'm using react-hook-form library with a multi-step-form
I tried getValues() in useEffect to update a state while changing tab ( without submit ) and it returned {}
useEffect(() => {
return () => {
const values = getValues();
setCount(values.count);
};
}, []);
It worked in next js dev, but returns {} in production
codesandbox Link : https://codesandbox.io/s/quirky-colden-tc5ft?file=/src/App.js
Details:
The form requirement is to switch between tabs and change different parameters
and finally display results in a results tab. user can toggle between any tab and check back result tab anytime.
Implementation Example :
I used context provider and custom hook to wrap setting data state.
const SomeContext = createContext();
const useSome = () => {
return useContext(SomeContext);
};
const SomeProvider = ({ children }) => {
const [count, setCount] = useState(0);
const values = {
setCount,
count
};
return <SomeContext.Provider value={values}>{children}</SomeContext.Provider>;
};
Wrote form component like this ( each tab is a form ) and wrote the logic to update state upon componentWillUnmount.
as i found it working in next dev, i deployed it
const FormComponent = () => {
const { count, setCount } = useSome();
const { register, getValues } = useForm({
defaultValues: { count }
});
useEffect(() => {
return () => {
const values = getValues(); // returns {} in production
setCount(values.count);
};
}, []);
return (
<form>
<input type="number" name={count} ref={register} />
</form>
);
};
const DisplayComponent = () => {
const { count } = useSome();
return <div>{count}</div>;
};
Finally a tab switching component & tab switch logic within ( simplified below )
const App = () => {
const [edit, setEdit] = useState(true);
return (
<SomeProvider>
<div
onClick={() => {
setEdit(!edit);
}}
>
Click to {edit ? "Display" : "Edit"}
</div>
{edit ? <FormComponent /> : <DisplayComponent />}
</SomeProvider>
);
}
I'm building a React app and I'd like to have a global CSS class that is used to fade in components when they appear in the viewport.
jQuery
With jQuery, I might do something like this:
const windowHeight = (window.innerHeight || document.documentElement.clientHeight);
const windowWidth = (window.innerWidth || document.documentElement.clientWidth);
isInViewport(el) {
const rect = el.getBoundingClientRect();
const vertInView = (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
const horInView = (rect.left <= windowWidth) && ((rect.left + rect.width) >= 0);
return (vertInView && horInView);
};
$(window).scroll(function(e) {
$('.animate').each(function() {
if(isInViewport($(this)[0])) {
$(this).addClass('animate--active');
}
});
});
On scroll, I'd check each element with the animate class and if that element is in the viewport, add the animate--active class to it, which will fade it in.
React
In React, I've moved my isInViewport() function to a global Helpers.js file so any component can make use of it, but I've had to add the scroll event and the dynamic class to every component, which makes for a lot of duplicated code. For example:
import { isInViewport } from './Helpers.js';
function MyComponent(props) {
const [inViewport, setInViewport] = useState(false);
const myComponentRef = useRef();
function handleScroll(e) {
setInViewport(isInViewport(myComponentRef.current));
}
useEffect(() => {
window.addEventListener('scroll', handleScroll);
// unmount
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
const classes = (inViewport) ? 'animate animate--active' : 'animate';
return (
<section className={classes} ref={myComponentRef}>
</section>
);
}
As far as I can tell, this would be the React way of doing this, and this does work, but again, it means that every component would require its own state variable, scroll event and class declaration, which adds up to a lot of repetition. Is there a better way of doing this?
Custom Hooks, Custom Hooks, Custom Hooks
import { isInViewport } from './Helpers.js';
function useIsInViewPort(ref) {
const [inViewport, setInViewport] = React.useState(false);
function handleScroll(e) {
setInViewport(isInViewport(ref.current));
}
React.useEffect(() => {
window.addEventListener('scroll', handleScroll);
// unmount
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return inViewport;
}
function Acmp(props) {
const ref = React.useRef();
const inViewport = useIsInViewPort(ref);
const classes = (inViewport) ? 'animate animate--active' : 'animate';
return (
<section className={classes} ref={ref}>
</section>
);
}
function Bcmp(props) {
const ref = React.useRef();
const inViewport = useIsInViewPort(ref);
return (
<section className={classes} ref={ref}>
</section>
);
}