Typescript React createContext and useState - javascript

I'm new to TS and trying to update code that I inherited from a former employee. I know it's TS related and have tried different types not able to resolve it. Any assistance would be great. Thanks in advance.
Here's the code:
import React from 'react';
export interface HistoryType {
history?: number;
setHistory: (value: number) => void;
}
const HistoryContext = React.createContext<HistoryType | undefined>(undefined);
export const HistoryProvider: React.FC = ({ children }) => {
const [history, setHistory] = React.useState();
return (
<HistoryContext.Provider value={{ history, setHistory}}>
{children}
</HistoryContext.Provider>
);
};
export const useHistoryState = () => {
const context = React.useContext(HistoryContext);
if (context === undefined) {
throw new Error('useHistoryState error');
}
return context;
};
Screenshot of the error:

const [history, setHistory] = React.useState()
Since no type is specified here, typescript has to try to infer it. It sees that undefined is implicitly being passed to use state, so it assumes the state is (and always will be) undefined. So that means that setHistory expects to be passed undefined.
You will need to specify the type yourself to say that it can either be a number, or undefined:
const [history, setHistory] = React.useState<number | undefined>();
Or if it's always supposed to be a number, you can do that too, but you'll need to provide an initial value which is a number:
const [history, setHistory] = React.useState<number>(42);
P.S: This is unrelated to your question, but i notice this code is employing a common mistake: You're creating a brand new object on every render and passing it as the context value.
value={{ history, setHistory }}>
Since there's a new value on every render, it will force anything that's consuming the context to rerender even if history and setHistory did not change. To fix this you should memoize the value so it only gets recomputed when it actually changes:
const value = React.useMemo(() => {
return { history, setHistory };
}, [history]);
return (
<HistoryContext.Provider value={value}>
{children}
</HistoryContext.Provider>
);

Related

How to indicate to TS that value in store has value even though it's optional?

I have the following case:
I have a standard store with optional items in it.
I also have a tree of elements which rely on that store. I also select {account} in multiple components.
For business logic, I had to check at the very top if account is set. If it is not, I don't render the components which rely on it.
How can I tell TS that even though the value is optional in store I'm 100% sure it is NOT undefined?
Example code:
// store
interface Account {
id: number;
name: string;
}
export interface AppState {
account?: Account;
}
const initialState: AppState = {};
const accountSlice = createSlice({
name: "account",
initialState,
reducers: {
setAccount(state: AppState, action: PayloadAction<Account | undefined>) {
state.account = action.payload;
}
}
});
// component
const GrandChild = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild /> : <>account not set</>;
};
export default function App() {
const dispatch = useDispatch();
useEffect(() => {
// dispatch(setAccount({ id: 0, name: "john" }));
});
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Child />
</div>
);
}
Codesandbox:
https://codesandbox.io/s/required-reducer-ysts49?file=/src/App.tsx
I know I can do this:
const { account } = useSelector((state: RootState) => state, shallowEqual) as Account;
but this seems very hacky. Is there a better way?
Well you know that Grandchild wont be rendered if account is undefined. so why not pass account as a property to the Grandchild component. This way you could define the property as never being undefined. And since you only render Grandchild after you checked that account isn't undefined you should be able to pass account as the property to the component (and since you defined the property as not being undefined TS will not object to account.name in your Grandchild component.
I don't know redux however - I have never used it and don't know anything about it, so I don't know if this answer is compatible with that or if redux will cause some issues I couldn't forsee.
I've written a little bit of code of how this could look (but as I already said, I don't know how to use redux, so you'll probably have to take my idea and write it so everything works) - so my code example is probably more of a visualization of what I mean than a solution.
const GrandChild = (account: Account) => {
return <>{account.name}</>;
};
const Child = () => {
const { account } = useSelector((state: RootState) => state, shallowEqual);
return account ? <GrandChild account={account} /> : <>account not set</>;
};

Using redux to take a time on a stopwatch and put it into a database

I was recommended using redux to place a time into a database but I'm having issues. I have a stopwatch in my index.js that the user can start and stop. After, I have a button that allows them the ability to to put their time into a database. From my node file, I'm getting UnhandledPromiseRejectionWarning: error: null value in column "time" violates not-null constraint. I'm wondering if I'm getting that because I have difference = 0 at the stop of the index file and it doesn't retrieve difference = this.state.endTime - this.state.startTime; or if there is another issue I'm not seeing.
index.js
export let difference = 0;
export default class Home extends React.Component {
constructor(props);
super(props);
this.state = {
startTime: 0,
endTime: 0,
}
}
handleStart = () => {
this.setState({
startTime: Date.now()
})
}
handleStop = () => {
this.setState({
endTime: Date.now()
})
}
render() {
difference = this.state.endTime - this.state.startTime;
return (
<App />
<reducer />
);
}
}
reducer.js
import * as actionTypes from "./actions.js";
import difference from "../pages/index.js";
const initialState = {
count: 0
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.NUMBER:
return { state, count: difference };
default: return state;
}
};
export default reducer;
store.js
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer);
export default store;
actions.js
export const NUMBER = 'NUMBER';
App.js
import React from 'react';
import { addName } from "./util";
function App() {
const [name, setName] = React.useState("")
function handleUpdate(evt) {
setName(evt.target.value);
}
async function handleAddName(evt) {
await addName(name);
}
return <div>
<p><input type='text' value={name} onChange={handleUpdate} /></p>
<button className='button-style' onClick={handleAddName}>Add Name</button>
</div>
}
export default App;
util.js
import "isomorphic-fetch"
import difference from "../pages/index.js";
export function addName(name) {
return fetch('http://localhost:3001/addtime', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, time: difference })
})
}
node server.js
app.post("/addtime", cors(), async (req,res) => {
const name = req.body.name;
const time = req.body.time;
const timeStamp = dateFormat(time, dateFormat.masks.isoDateTime);
const template = 'INSERT INTO times (name,time) VALUES ($1,$2)';
const response = await pool.query(template, [name,time]);
res.json({name: name, time: time});
});
State Management
In React we manage changing values through state. There is a way to access the value and a way to update the value. When the value changes, React knows to re-render the component.
This state can be stored within a component of your app. When you write const [name, setName] = React.useState(""), name is your "getter" and setName is your "setter".
The state can also be stored inside a Context Provider component that is a parent of the current component. Any children of this Provider can use the useContext hook to access the stored value.
The React Redux package uses these React contexts to make the redux state globally available. You wrap your entire App in a Redux Provider component to enable access.
With Redux, your "getters" are selector functions, which you call though useSelector, and your "setters" are actions that you pass though the dispatch function. React actually supports this syntax of state management on a local component level (without Redux) though the useReducer hook.
You don't need to use contexts in order to pass around state from component to component. You can also pass values and "setters" as props from a parent component to a child. React recommends this approach and has a section on Lifting State Up.
This is just a brief overview of some of the concepts at play. The issues that you are having here are due to state management. There are many possible ways to handle your state here. In my opinion Redux and Contexts are both unnecessary overkill. Of course you can use them if you want to, but you need to set them up properly which you haven't done.
Errors
export let difference = 0;
Variables which exist at the top-level of a file, outside of a component, should be immutable constants only. When you have a value that changes, it needs to be part of your app state.
When you have a "stateful" value like difference you can't just use a variable that exists outside of your components.
render() {
difference = this.state.endTime - this.state.startTime;
...
This is not how you update a value. We don't want to be just setting a constant. We also don't want to trigger changes of a stateful value on every render as this creates infinite re-render situations.
handleStart & handleStop
The functions themselves are fine but they are never called anywhere. Therefore difference will always be 0.
<reducer />
A reducer is not a component so you cannot call it like this. It is a pure function. In order to introduce redux into your JSX code, you use the reducer to create a store variable and you pass that store to a react-redux Provider component.
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.NUMBER:
return { state, count: difference };
default: return state;
}
};
You want to include the difference as a property of your action rather than accessing an external variable. The standard convention is to add a payload property to the action. The payload could be the difference number itself or an object with a property difference. That sort of design choice is up to you. An action might look like { type: actionTypes.NUMBER, payload: 0.065 } or { type: actionTypes.NUMBER, payload: { difference: 0.065 } }.
Your state is an object with a property count. Your reducer should return the next state, which should also be an object with a property count. This will not return the right state shape: return { state, count: difference };.
It is typical to use the spread operator ... to copy all other properties of the state and update just one (or a few), like this: return { ...state, count: difference }. However your state does not have any other properties, so that is the same as return { count: difference };. Since you are just storing a single number, there is no value from the previous state that is copied or preserved. (Which is a large part of why I think that Redux is not helpful or necessary here.)
There may be some issues on the backend as well, but there are such serious issues with the front end that I think that's your main problem.
Structuring Components
Think about what your app needs to do. What actions does it respond to? What information does it need to know?
Based on your description, it needs to:
Start a timer when a button is clicked
Stop a timer when a button is clicked
Store the most recent timer value
Allow a user to enter and update a name
Store that name
POST to the backend when a button is clicked, if there is a stored difference and name
Next you break that up into components. Store each stateful value at the highest component in the tree which needs to access it, and pass down values and callbacks as props. The startTime and endTime are only used by the timer itself, but the timer need to set the difference after stopping. The difference would be stored higher up because it is also needed for the POST request.
Think about what buttons and actions are available at any given time, and what conditions are used to determine this. For example, you shouldn't be able to stop a timer before you have started it. So we would see if startTime is greater than 0.
Minimal Front-End Example
import * as React from "react";
const Timer = ({ setDifference }) => {
const [startTime, setStartTime] = React.useState(0);
const handleStart = () => {
setStartTime(Date.now());
};
const handleStop = () => {
const difference = Date.now() - startTime;
setDifference(difference);
};
return (
<div>
<button onClick={handleStart}>
Start
</button>
<button disabled={startTime === 0} onClick={handleStop}>
Stop
</button>
</div>
);
};
const Submission = ({ time }) => {
const [name, setName] = React.useState("");
function handleUpdate(evt) {
setName(evt.target.value);
}
async function handleAddName(evt) {
return await fetch("http://localhost:3001/addtime", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, time })
});
}
return (
<div>
<div>Time: {time}</div>
<input type="text" value={name} onChange={handleUpdate} />
<button onClick={handleAddName} disabled={name.length === 0}>
Add Name
</button>
</div>
);
};
const App = () => {
const [time, setTime] = React.useState(0);
return (
<div>
<Timer setDifference={setTime} />
{time > 0 && <Submission time={time} />}
</div>
);
};
export default App;
CodeSandbox Link
You might not be saving any value to DOB column, you need to allow NULL value. You should change to
DOB = models.DateField(auto_now = False, null = True)
You should also make sure DOB column in your table allows null.

Difference between usePreviousDistinct with useEffect and without

I needed a hook to get the previous distinct value of a specific state. It looks like this and it seems to work:
function usePreviousDistinct(state) {
const prevRef = useRef();
useEffect(() => {
prevRef.current = state;
}, [state]);
return prevRef.current;
}
I've also seen there is a usePreviousDistinct hook in the react-use package but the approach is different than mine.
import { useRef } from 'react';
import { useFirstMountState } from './useFirstMountState';
export type Predicate<T> = (prev: T | undefined, next: T) => boolean;
const strictEquals = <T>(prev: T | undefined, next: T) => prev === next;
export default function usePreviousDistinct<T>(value: T, compare: Predicate<T> = strictEquals): T | undefined {
const prevRef = useRef<T>();
const curRef = useRef<T>(value);
const isFirstMount = useFirstMountState();
if (!isFirstMount && !compare(curRef.current, value)) {
prevRef.current = curRef.current;
curRef.current = value;
}
return prevRef.current;
}
I wonder if I have not understood something or am missing something. Is my version also correct?
In my test I could not find a difference:
https://codesandbox.io/s/distracted-mayer-zpym8?file=/src/App.js
useEffect() together with useRef() (your version) does not show the latest value.
useRef() without useEffect() gives you the correct value, because it runs synchronously, but doesn't have the advantages that come with asynchronicity.
useEffect() together with useState() gives you the correct value, but might trigger unnecessary renders (adds potentially unnecessary overhead).
Your version looks like it works as expected, because the old value that is shown is the one that you expect to see as the new value. But the actual new value is not the one you want.
Example:
import React, { useState, useEffect, useRef } from 'react';
export const MyComponent = function(props){
const [state, setState ] = useState(0);
return <React.Fragment>
state: { state },<br />
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
<button onClick={ function(){
setState( state + 1 );
} }>
increment
</button>
</React.Fragment>;
};
const usePreviousDistinctUE = function( value ){
const prevRef = useRef();
useEffect(() => {
prevRef.current = value;
console.log('with useEffect, prev:', prevRef.current, ', current:', value);
}, [value]);
return prevRef.current;
};
const usePreviousDistinctR = function( value ){
const prevRef = useRef();
const curRef = useRef( value );
if( curRef.current !== value ){
prevRef.current = curRef.current;
curRef.current = value;
}
console.log('w/o useEffect, prev:', prevRef.current, ', current:', curRef.current);
return prevRef.current;
};
The values shown on the page are the same, but in the console they are different. That means the value in the useEffect() version is changed, it is only not yet shown on the page.
If you just add another hook that updates anything unrelated (leaving everything else unchanged), then the page (might*) magically show the updated value again, because the page is re-rendered and the previously already changed value is shown. The value is now wrong in your eyes, but it is not changed, only shown:
// ...
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
anything updated: { useAnythingUpdating( state ) },<br />
// ...
const useAnythingUpdating = function(state){
const [result, setResult ] = useState(0);
useEffect(() => {
setResult( state );
console.log('anything updated');
});
return result;
};
*But you shouldn't rely on something else triggering a re-render. I'm not even sure this would update as expected under all circumstances.
more Details:
useEffect() is triggered at some time when react decides that the prop must have been changed. The ref is changed then, but react will not 'get informed' about a ref change, so it doesn't find it necessary to re-render the page to show the changed value.
In the example without useEffect() the change happens synchronously.
React doesn't 'know' about this change either, but (if everything else runs as expected) there will be always a re-render when necessary anyway (you will have called that function from another function that is rendered at the end).
(Not informing react about the change is basically the point in using useRef(): sometimes you want just a value, under your own control, without react doing magic things with it.)

React watch imported class property

I'm importing a plain class to my react (functional) component and want to be notified when an imported class property is set/updated. I've tried setting my imported class with just new, as a state variable with useState, as a ref with useRef - and have tried passing each one as a parameter to useEffect, but none of them are triggering the useEffect function when the property is updated a second time.
I've excluded all other code to drill down to the problem. I'm using Typescript, so my plain vanilla MyClass looks like this:
class MyClass {
userId: string
user: User?
constructor(userId: string){
this.userId = userId
// Do a network call to get the user
getUser().then(networkUser => {
// This works because I tried a callback here and can console.log the user
this.user = networkUser
}).catch(() => {})
}
}
And then in my component:
// React component
import { useEffect } from 'react'
import MyClass from './MyClass'
export default () => {
const myClass = new MyClass(userId)
console.log(myClass.user) // undefined here
useEffect(() => {
console.log(myClass.user) // undefined here and never called again after myClass.user is updated
}, [myClass.user])
return null
}
Again, this is greatly simplified. But the problem is that React is not re-rendering my component when the instance user object is updated from undefined to a User. This is all client side. How do I watch myClass.user in a way to trigger a re-render when it finally updates?
Let me guess you want to handle the business logic side of the app with OOP then relay the state back to functional React component to display.
You need a mechanism to notify React about the change. And the only way for React to be aware of a (view) state change is via a call to setState() somewhere.
The myth goes that React can react to props change, context change, state change. Fact is, props and context changes are just state change at a higher level.
Without further ado, I propose this solution, define a useWatch custom hook:
function useWatch(target, keys) {
const [__, updateChangeId] = useState(0)
// useMemo to prevent unnecessary calls
return useMemo(
() => {
const descriptor = keys.reduce((acc, key) => {
const internalKey = `##__${key}__`
acc[key] = {
enumerable: true,
configurable: true,
get() {
return target[internalKey]
},
set(value) {
if (target[internalKey] !== value) {
target[internalKey] = value
updateChangeId(id => id + 1) // <-- notify React about the change,
// the value's not important
}
}
}
return acc
}, {})
return Object.defineProperties(target, descriptor)
},
[target, ...keys]
)
}
Usage:
// React component
import { useEffect } from 'react'
import { useWatch } from './customHooks'
import MyClass from './MyClass'
export default () => {
const myClass = useMemo(() => new MyClass(userId), [userId])
useWatch(myClass, ['user'])
useEffect(() => {
console.log(myClass.user)
}, [myClass, myClass.user])
return null
}
Side Note
Not related to the question per se, but there're a few words I want to add about that myth I mentioned. I said:
props and context changes are just state change at a higher level
Examples:
props change:
function Mom() {
const [value, setValue] = useState(0)
setTimeout(() => setValue(v => v+1), 1000)
return <Kid value={value} />
}
function Dad() {
let value = 0
setTimeout(() => value++, 1000)
return <Kid value={value} />
}
function Kid(props) {
return `value: ${props.value}`
}
context change:
const Context = React.createContext(0)
function Mom() {
const [value, setValue] = useState(0)
setTimeout(() => setValue(v => v+1), 1000)
return (<Context.Provider value={value}>
<Kid />
</Context.Provider>)
}
function Dad() {
let value = 0
setTimeout(() => value++, 1000)
return (<Context.Provider value={value}>
<Kid />
</Context.Provider>)
}
function Kid() {
const value = React.useContext(Context)
return `value: ${value}`
}
In both examples, only <Mom /> can get <Kid /> to react to changes.
You can pass this.user as props and use props,.user in useEffeect. You could do that from the place getUser called.
A wholesome solution would be using a centralized state solution like redux or context API. Then you need to update store in getUser function and listen globalstate.user.
Conclusion
You need to pass this.user to the component one way or another. You need to choose according to the project.

React Tooltip component with TypeScript getting a 'Possibly Undefined' error on my useRef element, not sure how to fix

I have a simple functional React component for a re usable tooltip that I'm working on and I've got everything almost ready to test, but I'm getting a TypeScript error 'Possibly Undefined' on my useRef element node at this line in my function: if (node.current.contains(target))... and I don't know how to fix it.
I initially tried setting the type of useRef as:
const node = useRef()<HTMLDivElement>(null);
and then calling it in my function like:
const handleHover = ({ target }) => {
if (node?.current.contains(target)) {
....
}
}
But that throws an error "Cannot invoke an expression whose type lacks a call signature. Type 'MutableRefObject' has no compatible call signatures." at the definition level and then "Expected an expression" in my function.
I'm certain the answer is obvious and I have just been staring at this for too long but I'm stumped at the current moment so any feedback is hugely appreciated.
My current component for full reference:
import React, { useEffect, useRef, useState } from 'react';
import './tooltip-style.scss'
import cx from 'classnames';
type Props = {
title: string;
position: string;
children: React.ReactNode;
};
const Tooltip = (props: Props) => {
const { title, position, children } = props;
const node = useRef();
const [isVisible, setState] = useState(false);
const handleHover = ({ target }) => {
if (node.current.contains(target)) {
// hover over
return;
}
// hover out
setState(false);
};
useEffect(() => {
document.addEventListener('mouseover', handleHover);
return () => {
document.removeEventListener('mouseout', handleHover);
};
}, []);
return (
<div className={'.container'}
data-testid="tooltip"
ref={node}
onMouseEnter={() => setState(!isVisible)}
>
<div data-testid="tooltip-placeholder">{children}</div>
{isVisible && (
<div
className={cx('.tooltipContent', [position])}
data-testid="tooltip-content"
>
<span className={'.arrow'}></span>
{title}
</div>
)}
</div>
);
};
export default Tooltip;
You can solve this by typing the ref as follows (using your example):
const node = useRef() as MutableRefObject<HTMLDivElement>;
Just be sure you have the necessary imports as well:
import React, { useRef, MutableRefObject } from 'react';
You are almost there but there are two syntactical errors.
const node = useRef()<HTMLDivElement>(null);
useRef() calls the function and then you call the result of that function with (null). You need to remove those first parentheses and just call the hook once:
const node = useRef<HTMLDivElement>(null);
if (node?.current.contains(target)) {
The variable node is always defined. It is the current property which might be null. So move the optional chaining question mark to after current:
if (node.current?.contains(target)) {
If you want to add a type to the arguments of handleHover there's a minor issue because the target property of a MouseEvent is either EventTarget or null. But .contains() expects a Node. I'm not sure if you would treat invalid arguments as "hover over" or "hover out". It's unlikely to occur in reality but to be type-safe we should check it.
const node = useRef<HTMLDivElement>(null);
const [isVisible, setState] = useState(false);
const handleHover = ({ target }: MouseEvent) => {
if (target instanceof Node && node.current?.contains(target)) {
// hover over
return;
}
// hover out
setState(false);
};
Typescript Playground Link

Categories

Resources