How do you handle external state with React Hooks? - javascript

I have a mathematical algorithm that I want to keep separated from React. React would be a view to the state within that algorithm, and should not define the way of how the logic is flowing within the algorithm. Also, since it is separated, it's much easier to unit test the algorithm. I have implemented it using class components (simplified):
class ShortestPathRenderer extends React.Component {
ShortestPath shortestPath;
public constructor(props) {
this.shortestPath = new ShortestPath(props.spAlgorithm);
this.state = { version: this.shortestPath.getVersion() };
}
render() {
... // Render waypoints from shortestPath
}
onComponentDidUpdate(prevProps) {
if (prevProps.spAlgorithm !== this.props.spAlgorithm) {
this.shortestPath.updateAlgorithm(this.props.spAlgorithm);
}
}
onComponentWillUnmount() {
this.shortestPath = undefined;
}
onAddWayPoint(x) {
this.shortestPath.addWayPoint(x);
// Check if we need to rerender
this.setState({ version: this.shortestPath.getVersion() });
}
}
How would I go about this using React hooks? I was thinking about using the useReducer method. However, the shortestPath variable would then be a free variable outside the reducer and the reducer is no longer pure, which I find dirty. So in this case the whole state of the algorithm must be copied with every update on the internal state of the algorithm and a new instance must be returned, which is not efficient (and forces the logic of the algorithm to be the React-way):
class ShortestPath {
...
addWayPoint(x) {
// Do something
return ShortestPath.clone(this);
}
}
const shortestPathReducer = (state, action) => {
switch (action.type) {
case ADD_WAYPOINT:
return action.state.shortestPath.addWayPoint(action.x);
}
}
const shortestPathRenderer = (props) => {
const [shortestPath, dispatch] = useReducer(shortestPathReducer, new ShortestPath(props.spAlgorithm));
return ...
}

You can switch class-based in example in functional analog just using useState hook
function ShortestPathRenderer({ spAlgorithm }) {
const [shortestPath] = useRef(new ShortestPath(spAlgorithm)); // use ref to store ShortestPath instance
const [version, setVersion] = useState(shortestPath.current.getVersion()); // state
const onAddWayPoint = x => {
shortestPath.current.addWayPoint(x);
setVersion(shortestPath.current.getVersion());
}
useEffect(() => {
shortestPath.current.updateAlgorithm(spAlgorithm);
}, [spAlgorithm]);
// ...
}

I'd go with something like this:
const ShortestPathRenderer = (props) => {
const shortestPath = useMemo(() => new ShortestPath(props.spAlgorithm), []);
const [version, setVersion] = useState(shortestPath.getVersion());
useEffect(() => {
shortestPath.updateAlgorithm(spAlgorithm);
}, [spAlgorithm]);
const onAddWayPoint = (x) => {
shortestPath.addWayPoint(x);
// Check if we need to rerender
setVersion(shortestPath.getVersion());
}
return (
... // Render waypoints from shortestPath
)
}
you can even decouple logic further and create useShortestPath hook:
reusable stateful logic:
const useShortestPath = (spAlgorithm) => {
const shortestPath = useMemo(() => new ShortestPath(spAlgorithm), []);
const [version, setVersion] = useState(shortestPath.getVersion());
useEffect(() => {
shortestPath.updateAlgorithm(spAlgorithm);
}, [spAlgorithm]);
const onAddWayPoint = (x) => {
shortestPath.addWayPoint(x);
// Check if we need to rerender
setVersion(shortestPath.getVersion());
}
return [onAddWayPoint, version]
}
presentational part:
const ShortestPathRenderer = ({spAlgorithm }) => {
const [onAddWayPoint, version] = useShortestPath(spAlgorithm);
return (
... // Render waypoints from shortestPath
)
}

Related

Notify other component by raising a manual event

A have two files, with two functional components A and B, in the first component A, i have a specialFunction that gets called with onClick, what i want to do is raise an event in specialFunction when it's called, and then in component B add a Listener for the event in specialFunction.
Component A:
function specialFunction(){
//raise the event and send some data
}
Component B:
//contains a listener that does some work when specialFunction is called, example:
(data) => {console.log("am called:",data)};
1. Create notifier class using observer pattern
class ChangeNotifier {
subscribers = [];
subscribe(callback) {
this.subscribers.push(callback);
}
unsubscribe(callback) {
const index = this.subscribers.indexOf(callback);
if (index > -1) {
this.subscribers.splice(index, 1);
}
}
notifyAll(data) {
this.subscribers.forEach(callback => callback(data));
}
}
2. ComponentA receives notifier as a prop and used to notify all subscribers
const ComponentA = ({ notifier }) => {
const triggerNotifier = () => {
notifier.notifyAll('Some data that will subscribers receive');
}
return <div>{/** Some content */}</div>
}
3. ComponentB receives notifier and subscribes to it to receive data sent by from ComponentB
const ComponentB = ({ notifier }) => {
useEffect(() => {
const callbackFn = data => {/** Do whatever you want with received data */ }
notifier.subscribe(callbackFn);
return () => notifier.unsubscribe(callbackFn);
}, [])
}
4. App holds both component. Create instance of notifier there and pass as a props
const App = () => {
const dataNotifier = new ChangeNotifier();
return <div>
<ComponentA notifier={dataNotifier} />
<ComponentB notifier={dataNotifier} />
</div>
}
If you have components on different levels deeply nested and it is hard to pass notifier as a prop, please read about React Context which is very helpful when you want to avoid property drilling
React Context
Here's implementation with context
class ChangeNotifier {
subscribers = [];
subscribe(callback) {
this.subscribers.push(callback);
return this.unsubscribe.bind(this, callback);
}
unsubscribe(callback) {
const index = this.subscribers.indexOf(callback);
if (index > -1) {
this.subscribers.splice(index, 1);
}
}
notifyAll(data) {
this.subscribers.forEach(callback => callback(data));
}
}
const NotifierContext = React.createContext();
const ComponentA = () => {
const { notifier } = useContext(NotifierContext);
const triggerNotifier = () => {
notifier.notifyAll('Some data that will subscribers receive');
}
return <div><button onClick={triggerNotifier}>Notify</button></div>
}
const ComponentB = () => {
const { notifier } = useContext(NotifierContext);
useEffect(() => {
const callbackFn = data => { console.log(data) }
notifier.subscribe(callbackFn);
return () => notifier.unsubscribe(callbackFn);
}, [notifier])
}
Now all components wrapped in NotifierContext.Provider (no matter how deep they are nested inside other components) will be able to use useContext hook to receive context value passed as value prop to NotifierContext.Provider
const App = () => {
const dataNotifier = useMemo(() => new ChangeNotifier(), []);
return <NotifierContext.Provider value={{ notifier: dataNotifier }}>
<ComponentA />
<ComponentB />
</NotifierContext.Provider>
}
export default App;
Last but not least, I guess you can avoid context or properties drilling and just create instance of ChangeNotifier in some utility file and export it to use globally...
Andrius posted a really good answer, but my problem was that the two components, one of them is used as an API, and the other had a parent component, am a beginner so maybe there is a way to use them but i just didn't know how.
The solution that i used, (maybe not the best) but did the job was to dispatch a custom event in a Promise from the specialFunction:
function specialFunction(){
new Promise((resolve) => {
console.log("am the promise");
document.dispatchEvent(event);
resolve();
});
And add a Listener in the other component using a useEffect hook:
useEffect(() => {
let handlePreview = null;
new Promise((resolve) => {
document.addEventListener(
"previewImg",
(handlePreview = (event) => {
event.stopImmediatePropagation();
//Stuff...
})
);
return () =>
window.removeEventListener("previewImg", handlePreview, false);
});
}, []);
Thank you for your help.

how can i refactor this code snippet from a class to functional component react?

i have this code inside a class, how could i keep the idea of ​​it but updating to use in a function component? I'm trying to change but I can't keep the current proposal
validate = value => {
const {
formApi: { getValue },
name,
} = this.props;
const component = (props) => {
const validate = value => {
const {
formApi: { getValue },
name,
} = props
}
}
const Component = ({formApi: {getValue}, name})=> {
const validate = useCallback((value)=> {
}, [getValue, name]);
}

Can not set a state in a react component

I have a react component. I want to set the state within this component that will be passed down to child components. I am getting a reference error to this and I am not sure why.
export const WidgetToolbar: React.FC<{}> = () => {
this.state = {
targetBox:null,
}
const isOpen = useBehavior(mainStore.isWidgetToolbarOpen);
const themeClass = useBehavior(mainStore.themeClass);
const userDashboards = useBehavior(dashboardStore.userDashboards);
const [filter, setFilter] = useState("");
const [sortOrder, setSortOrder] = useState<SortOrder>("asc");
const userWidgets = useMemo(() => {
let _userWidgets = values(userDashboards.widgets).filter((w) => w.widget.isVisible);
if (sortOrder === "asc") {
_userWidgets.sort((a, b) => a.widget.title.localeCompare(b.widget.title));
} else {
_userWidgets.sort((a, b) => b.widget.title.localeCompare(a.widget.title));
}
if (!isBlank(filter)) {
_userWidgets = _userWidgets.filter((row) => {
return row.widget.title.toLowerCase().includes(filter.toLocaleLowerCase());
});
}
return _userWidgets;
}, [userDashboards, sortOrder, filter]);
...
This is the error I am getting:
TypeError: Cannot set property 'state' of undefined
at WidgetToolbar (WidgetToolbar.tsx?ba4c:25)
at ProxyFacade (react-hot-loader.development.js?439b:757)
There's no this or this.state in a functional component. Use the useState hook, similar to what you're doing a few lines below.
export const WidgetToolbar: React.FC<{}> = () => {
const [targetBox, setTargetBox] = useState<null | whateverTheTypeIs>(null);
//...
}
Functional React Components can't have state. You'd have to use a class-based component in order to have state.
https://guide.freecodecamp.org/react-native/functional-vs-class-components/
You used the hook to "use state" in this function: const [filter, setFilter] = useState("");
You could do the same for targetBox, instead of trying to set a property on a non-existent 'this'

How to test an unmounted React component's method that relies on setState?

I have implemented a component (for a typing training app) which tracks key presses on global scope like this:
class TrainerApp extends React.Component {
constructor() {
// ...
this.handlePress = this.handlePress.bind(this);
}
handlePress(event) {
const pressedKey = event.key;
const task = this.state.task;
const expectedKey = task.line[task.position];
const pressedCorrectly = pressedKey == expectedKey;
this.setState(prevState => {
const newPosition = prevState.task.position +
(pressedCorrectly ? 1 : 0);
return {
// ...prevState, not needed: https://reactjs.org/docs/state-and-lifecycle.html#state-updates-are-merged
task: {
...prevState.task,
position: newPosition,
mistakeAtCurrentPosition: !pressedCorrectly
}
}
})
}
componentDidMount() {
document.addEventListener(this.keyEventTypeToHandle,this.handlePress);
}
componentWillUnmount () {
document.removeEventListener(this.keyEventTypeToHandle,this.handlePress);
}
...
}
and I'd like to write some unit-tests using Jest. My initial idea was:
describe('TrainerApp.handlePress should',() => {
test('move to the next char on correct press',() => {
const app = new TrainerApp();
app.state.task.line = 'abc';
app.state.task.position = 0;
const fakeEvent = { key: 'a' };
app.handlePress(fakeEvent);
expect(app.state.task.position).toBe(1);
});
...
});
but the problem is app.handlePress relies on usage of this.setState which is not defined when the component is not mounted yet. Of'course I can modify the app like this:
test('move to the next char on correct press',() => {
const app = new TrainerApp();
app.setState = jest.fn(function(handler) {
this.state = handler(this.state);
});
app.state.task.line = 'abc';
app.state.task.position = 0;
const fakeEvent = { key: 'a' };
app.handlePress(fakeEvent);
expect(app.state.task.position).toBe(1);
});
or even like this:
class ExplorableTrainerApp extends TrainerApp {
setState(handler) {
this.state = handler(this.state);
}
}
test('move to the next char on correct press',() => {
const app = new ExplorableTrainerApp();
app.state.task.line = 'abc';
app.state.task.position = 0;
const fakeEvent = { key: 'a' };
app.handlePress(fakeEvent);
expect(app.state.task.position).toBe(1);
});
but this seems a very fragile approach (here I rely on the fact that .setState is called with the function argument while it can be called with just newState argument and hence I'm testing implementation details, instead of just the behaviour. Is there a more robust way to test this?
There are a few frameworks for testing React components, Enzyme and react-testing-library are both popular and well supported.

Is there such a thing as a "correct" way of defining state with React hooks and Typescript?

I've been working with React for a while, and yesterday I got my feet wet with hooks in a Typescript based project. Before refactoring, the class had a state like this:
interface INavItemProps {
route: IRoute;
}
interface INavItemState {
toggleStateOpen: boolean
}
class NavItem extends Component<INavItemProps, INavItemState> {
constructor() {
this.state = { toggleStateOpen: false };
}
public handleClick = (element: React.MouseEvent<HTMLElement>) => {
const toggleState = !this.state.toggleStateOpen;
this.setState({ toggleStateOpen: toggleState });
};
...
}
Now, when refactoring to a functional component, I started out with this
interface INavItemProps {
route: IRoute;
}
const NavItem: React.FunctionComponent<INavItemProps> = props => {
const [toggleState, setToggleState] = useState<boolean>(false);
const { route } = props;
const handleClick = (element: React.MouseEvent<HTMLElement>) => {
const newState = !toggleState;
setToggleState(newState);
};
...
}
But then I also tested this:
interface INavItemProps {
route: IRoute;
}
interface INavItemState {
toggleStateOpen: boolean
}
const NavItem: React.FunctionComponent<INavItemProps> = props => {
const [state, setToggleState] = useState<INavItemState>({toggleStateOpen: false});
const { route } = props;
const handleClick = (element: React.MouseEvent<HTMLElement>) => {
const newState = !state.toggleStateOpen;
setToggleState({toggleStateOpen: newState});
};
...
}
Is there such a thing as a correct way of defining the state in cases like this? Or should I simply just call more hooks for each slice of the state?
useState hook allows for you to define any type of state like an Object, Array, Number, String, Boolean etc. All you need to know is that hooks updater doesn't merge the state on its own unline setState, so if you are maintain an array or an object and you pass in only the value to be updated to the updater, it would essentially result in your other states getting lost.
More often than not it might be best to use multiple hooks instead of using an object with one useState hook or if you want you can write your own custom hook that merges the values like
const useMergerHook = (init) => {
const [state, setState] = useState(init);
const updater = (newState) => {
if (Array.isArray(init)) {
setState(prv => ([
...prv,
...newState
]))
} else if(typeof init === 'object' && init !== null) {
setState(prv => ({
...prv,
...newState
}))
} else {
setState(newState);
}
}
return [state, updater];
}
Or if the state/state updates need to be more complex and the handler need to be passed down to component, I would recommend using useReducer hook since you have have multiple logic to update state and can make use of complex states like nested objects and write logic for the update selectively

Categories