I am learning FP and I am trying to figure out how to handle events in react. For example, let's use following scenario:
interface Todo {
task: string
done: boolean
}
interface TodoProps {
todo: Todo
onChange: ChangeEventHandler<HTMLInputElement>
}
function TodoItem({todo, onChange}: TodoProps) {
return (
<label>
{todo.task}
<input type="checkbox" checked={todo.done} onChange={onChange}/>
</label>
)
}
function App() {
const [todo, setTodo] = useState({
task: "Some task",
done: false
});
const toggleTodo = // I need to implement this
return (
<main>
<TodoItem onChange={toggleTodo} todo={todo}/>
</main>
)
}
Nothing fancy, just basic todo app.
In the missing function I need to create object with updated done
property. To do that I create Ramda's lens focused on done
property.
const doneLens = lensProp('done');
Then it should be easy to finish the goal. All I need is to compose setTodo
with Rambda's over
.
const toggleDone = () => compose(setTodo, over(doneLens, toggleBoolean))(todo)
But here is the issue. I am getting this ts error:
TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type '<T>(value: T) => T' is not assignable to parameter of type '(x0: unknown, x1: unknown, x2: unknown) => SetStateAction<{ task: string; done: boolean; }>'.
Type 'unknown' is not assignable to type 'SetStateAction<{ task: string; done: boolean; }>'.
Type 'unknown' is not assignable to type '(prevState: { task: string; done: boolean; }) => { task: string; done: boolean; }'.
In pure js this function should work, but ts can't infer return type of over
function. It's logical. The over
is generic so let's try to add explicit type.
const toggleDone = () => compose(setTodo, over<Todo>(doneLens, toggleBoolean))(todo)
And I get:
TS2554: Expected 3 arguments, but got 2.
index.d.ts(669, 51): An argument for 'value' was not provided.
TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'Todo' is not assignable to parameter of type '(x0: unknown, x1: unknown, x2: unknown) => SetStateAction<{ task: string; done: boolean; }>'.
Type 'Todo' provides no match for the signature '(x0: unknown, x1: unknown, x2: unknown): SetStateAction<{ task: string; done: boolean; }>'.
Ramda functions are curried by default, but if I can read the error correctly it seem that when I add explicit type then the currying does not working.
I can come up with workaround:
const overDone: (t: Todo) => Todo = over(doneLens, toggleBoolean);
const toggleDone = () => compose(setTodo, overDone)(todo);
This works because return type of overDone
match with input type of setTodo
.
But, my question is. How to fix the oneliner? Or if you know better way to handle similar scenarios with lenses, function composition, useState hook and typescript I am curious to see so.
CodePudding user response:
Add the type Todo
to the lensProp
call. You can also move it out of the component, because you don't need to generate the function whenever the component re-renders:
const doneLens = lensProp<Todo>("done");
const toggleDone = over(doneLens, not);
Since setTodo
is a function that can call another function (see https://reactjs.org/docs/hooks-reference.html#functional-updates) to which it passes the current todo
, you don't need R.compose
:
const toggleTodo = () => setTodo(over(doneLens, not));
And this how your component would look (sandbox):
const doneLens = lensProp<Todo>("done");
const toggleDone = over(doneLens, not);
function App() {
const [todo, setTodo] = useState({
task: "Some task",
done: false
});
const toggleTodo = () => setTodo(toggleDone);
return (
<main>
<TodoItem onChange={toggleTodo} todo={todo} />
</main>
);
}
A simpler option would be to use R.evolve
(sandbox):
const toggleDone = evolve({ done: not });
function App() {
const [todo, setTodo] = useState({
task: "Some task",
done: false
});
const toggleTodo = () => setTodo(toggleDone);
console.log(todo);
return (
<main>
<TodoItem onChange={toggleTodo} todo={todo} />
</main>
);
}