Skip to main content

· 5 min read

Motivation

The useState hook is a convenient method to provide a temporary component state. It is very common to invoke the useState hook for added interactivity of a button or other visual components. There are 3 main concerns when using the useState hook.

  • Is the state necessary?
  • Is the state located at the right level in the component tree?
  • Is the state updated correctly?

The first 2 questions can be answered with some considerations on the choice of state management solutions and whether to use composition vs inheritance. They are somewhat discussed here in Composition vs Inheritance , Lifting State up and Thinking in React. I would like to share a little bit on updating the state properly (which I failed to do so in a new feature that I was implementing).

Encounter

I was adding a search + sort feature to display a list of quizzes in my side project done in Next.js (which is practically React). To keep track of the search input and the sorting option, I used two useState hooks. I have extracted out the related piece of code below, style related classes and components are left out for brevity.

In gist :

  • QuizList component receives a list of quizzes
  • query keeps track of the search input
  • sortBy keeps track of the sorting option
  • filteredQuizzes keeps track of the manipulated copy of quizzes to be displayed
const QuizList = ({ quizzes }: { quizzes: Quiz[] }): JSX.Element => {
const [query, setQuery] = useState('')
const [sortBy, setSortBy] = useState('title')
const [filteredQuizzes, setFilteredQuizzes] = useState<Quiz[]>([])
useEffect(() => {
let result = quizzes
if (sortBy === 'title') {
result.sort((a, b) => a.title.localeCompare(b.title))
} else {
result.sort((a, b) => a.week - b.week)
}
if (query.trim() === '') {
setFilteredQuizzes(result)
} else {
setFilteredQuizzes(
result.filter((quiz) => quiz.title.toLowerCase().includes(query.toLowerCase()))
)
}
}
}, [query, quizzes, sortBy])
return (
<div>
<Search query={query} setQuery={setQuery} />
<RadioGroup onChange={setSortBy} value={sortBy}>
<Stack direction="row">
<span>Sort by:</span>
<Radio value="title">Title</Radio>
<Radio value="week">Week</Radio>
</Stack>
</RadioGroup>
<div>
{filteredQuizzes.map((quiz) => {
return <QuizItemCard key={quiz.id} quiz={quiz} />
})}
</div>
</div>
)

This is how it looks like: before

I must say that the few times I decided to violate the principle of immutability and start my code by declaring variables with let instead of const, they always surprised me with a hidden bug. The above code seemed to work but there was a strange lag when I toggle between the options for sorting by 'Title' vs sorting by 'Week'. In fact, the sorting seemed to be erroneous.

The logic of the code is as follows:

  • check the sorting option, if it is sort by title, sort the list of quizzes in place with a comparison on the titles. Else, sort the list by the week attribute of each quiz
  • then check for search input and keep only the ones that include the search input

I suspected that the inconsistent and delayed sorting behavior was due to the mutation of quiz list in place and the wrong use of setFilteredQuizzes. Coincidentally, the article that I planned to write this week was related to the official React.js FAQ and reading its section on Component State gave me an idea on how to fix the state update.

Fix

As mentioned in the Component State section of the FAQ, setState operations are not immediately invoked and inappropriate usage will result in unintended consequences. Quoting an example code snippet directly from the document:

incrementCount() {
this.setState((state) => {
// Important: read `state` instead of `this.state` when updating.
return {count: state.count + 1}
});
}

handleSomething() {
// Let's say `this.state.count` starts at 0.
this.incrementCount();
this.incrementCount();
this.incrementCount();

// If you read `this.state.count` now, it would still be 0.
// But when React re-renders the component, it will be 3.
}

Because of the above fact, it helps to know that the setState operation can accept either the updated state or a update function that will take in the previous state and return the updated state.

Thus my solution is simple: use the spread operator to make a copy of the list, sort it and return it within an update function. By making updates within the update functions, the latest updated list will be used whenever filteredQuizzes is referenced. Other alternative solutions include updating the filteredQuizzes in event handlers of the sorting radio buttons instead of keeping track of the sorting state.

The fixed version looks like this: after

And the code as follows:

const QuizList = ({ quizzes }: { quizzes: Quiz[] }): JSX.Element => {
const [query, setQuery] = useState('')
const [sortBy, setSortBy] = useState('title')
const [filteredQuizzes, setFilteredQuizzes] = useState<Quiz[]>([])
useEffect(() => {
if (sortBy === 'title') {
setFilteredQuizzes(() => [...quizzes].sort((a, b) => a.title.localeCompare(b.title)))
} else {
setFilteredQuizzes(() => [...quizzes].sort((a, b) => a.week - b.week))
)
}
if (query.trim() === '') {
setFilteredQuizzes((filteredQuizzes) => filteredQuizzes)
} else {
setFilteredQuizzes((filteredQuizzes) =>
filteredQuizzes.filter((quiz) => quiz.title.toLowerCase().includes(query.toLowerCase()))
)
}
}, [query, quizzes, sortBy])
return (
<div>
<Search query={query} setQuery={setQuery} />
<RadioGroup onChange={setSortBy} value={sortBy}>
<Stack direction="row">
<span>Sort by:</span>
<Radio value="title">Title</Radio>
<Radio value="week">Week</Radio>
</Stack>
</RadioGroup>
<div>
{filteredQuizzes.map((quiz) => {
return <QuizItemCard key={quiz.id} quiz={quiz} />
})}
</div>
</div>
)

Conclusion

As someone who is guilty of being lazy, my initial response to the above bug after some experimentation was to remove the sorting option and just sort the quizzes coming in. Due to the fact that I set out to write this article, and that I was inspired by Kent C. Dodds to read the React FAQ, I avoided taking the easy way out and spent some more time thinking about this little problem.

I guess the moral of the story is:

  • Don't always take the easy way out
  • Give problems a second thought
  • Start writing articles

· 3 min read

Motivation

const genericFn = <T>() => {
return "This is a poorly written example generic function"
}

Above is an example of a function with a generic parameter T that could potentially be used within the function body. However, if the above code is saved in a .tsx file (In my context, within a React application, while trying to create a generic custom hook), you will receive with the following error when hover over <T>:

JSX element 'T' has no corresponding closing tag.ts(17008)
Cannot find name 'T'.ts(2304)

Exploration

Defining generic function in TypeScript

To resolve the issue, I started with researching on how to properly define a generic function in TypeScript. Perhaps I made a mistake somewhere in the above syntax. I landed on two articles here and here. Both of them talked about how to create a custom React hook that uses generics. However, the syntax used in the articles were similar to the above example but no errors were discussed.

While I did not find an answer after heading over to TypeScript-CheatSheets, I thought the note on avoiding type inference when declaring custom hook was an interesting point that I did not know about.

If you are returning an array in your Custom Hook, you will want to avoid type inference as TypeScript will infer a union type (when you actually want different types in each position of the array). Instead, use TS 3.4 const assertions:

export function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as const; // infers [boolean, typeof load] instead of (boolean | typeof load)[]
}

This way, when you destructure you actually get the right types based on destructure position.

Credit: TypeScript-CheatSheets


Google the error

Moving on with the second strategy: "Google & Stack overflow". Searching the above error landed me on the following issue in the Microsoft TypeScript repository. There were a few more interesting links here and here.

So according to the reported issue:

The usage of <T> prior to the function braces causes a JSX error within .tsx files: "JSX element has no corresponding closing tag.". Basic example works as expected in a .ts file.

The issue was claimed to be a limitation and there were a few workarounds mentioned (in the thread and also in the related stack overflow post):

  • change from .tsx to .ts
  • add a comma: const f = <T,>(arg: T): T => {...}
  • extend this way: const foo = <T extends unknown>(x: T) => x;
  • or extend this way: const foo = <T extends {}>(x: T): T => x;

Thoughts

Funny how issues like this one will continue to bite us even way into the future... simply because no one is going to do anything about it?

· 4 min read

With this series I intend to note down some of the confusion and quirky stuff that I encountered out in the wild. So, today I am going to start with this snippet in TypeScript.

Motivation

interface CustomState {
value: {
[key:string]: any
}
}

const defaultState : CustomState = {
value: {}
}

const reducer = (state: CustomState, action: { type: string }): CustomState => {
if (action.type === 'reset') {
return {
value: []
}
} else {
return {
...state
}
}
}

The CustomState declared at the start includes a property called value, which is an object with key-value pairs of the form string - any. The defaultState variable contains an (empty) object conforming to the interface declared above, which is perfectly normal.

The thing that caught me off-guard is in the reducer. The reducer function is supposed to reset the state by clearing out the value property. However, notice here that an array [] is used, instead of {}.

I thought the change from type object to type array is pretty drastic, especially if I compare it to Java (Changing from a HashMap to an ArrayList just like that? Is this even allowed?). The strangest part of this was that TypeScript had no qualms about this at all. No curly lines nor compiler warnings.


Exploration

The first thing I did was to find out whether the interface was declared correctly, i.e. is it declaring that value contains an object or an array. This led me to review the definition of index signature.

Index Signature

Index signature is a way to describe the types of possible values. Borrowing the examples used in the official TypeScript docs:

interface StringArray {
[index: number]: string;
}

const myArray: StringArray = getStringArray();
const secondItem = myArray[1]; // secondItem is of type string

The syntax to declare the index signature might seem strange at first. It looks like declaring an object with {} but in the above example, it is used for declaring an interface for an array. For comparison, the way to declare an object interface looks like this:

interface PaintOptions {
xPos: number;
yPos: number;
}

In the TypeScript documentation example, index signature is used to describe an array. However, there is also a line below that says:

While string index signatures are a powerful way to describe the “dictionary” pattern, they also enforce that all properties match their return type.

Doing a bit more research would point me to other examples of how index signatures are also applicable to objects:

interface NumberDictionary {
[index: string]: number;
length: number;
width: number;
}

So in the case of CustomState, both the following usage are correct:

const arrayExample:CustomState = {
value: [{val: 1}]
}

const objectExample:CustomState = {
value: {val: 1}
}

Array VS Object

The second thing I checked was that since {} could be replaced with [], are arrays and objects, besides what we already know about the different use cases, the same thing in JavaScript/TypeScript? Without going too deep into this question, we can make an observation with console log :

console.log(typeof []) // "object"
console.log(typeof {}) // "object"

Stack Overflow?

The last bit of things of interest came up when I started to draft examples for this article and encountered this stack overflow question. Essentially, the person had an issue with Index signature of object type implicitly has an 'any' type. Scrolling further down, an proposed answer had something similar to my initial example:

type ISomeType = {[key: string]: any};

let someObject: ISomeType = {
firstKey: 'firstValue',
secondKey: 'secondValue',
thirdKey: 'thirdValue'
};

let key: string = 'secondKey';

let secondValue: string = someObject[key];

In fact to add on, the declaration of ISomeType allows for the following to work as well:

type ISomeType = {[key: string]: any};

// My additional example
let someArray: ISomeType = [
{firstKey: 'firstValue'},
{secondKey: 'secondValue'},
{thirdKey: 'thirdValue'}
]

let newkey: string = 'secondKey';

let newSecondValue: string = someArray[newkey];

But, if the use of any has been replaced, the whole thing would break:

// Note the change in the type and therefore the error!
type ISomeTypeA = {[key: string]: string};

let someObjectA: ISomeTypeA = {
firstKey: 'firstValue',
secondKey: 'secondValue',
thirdKey: 'thirdValue'
};

let keyA: string = 'secondKey';

let secondValueA: string = someObjectA[keyA];

// My additional example
let someArrayA: ISomeTypeA = [ // Error: Type '{ firstKey: string; }' is not
{firstKey: 'firstValue'}, // assignable to type 'string'.
{secondKey: 'secondValue'},
{thirdKey: 'thirdValue'}
]

let newkeyA: string = 'secondKey';

let newSecondValueA: string = someArrayA[newkeyA];

Potential moral of the story? Don't use any 😂


Resources

The code snippets used in this article is also available at this TypeScript playground.