Hey,
I am trying to use the new react hooks with Ink components, but I get the error:
Invariant Violation: Hooks can only be called inside the body of a function component.
I am using the hooks inside a function component, so I guess it is related to Ink implementation?
Here is the code:
import React, {useState} from 'react'
import {h, render, Component} from "ink";
function useTest() {
const [value, update] = useState(Date.now());
return [value, update]
}
const Demo = () => {
const [value, update] = useTest();
setInterval(() => {
update(Date.now())
}, 1000)
return <div>Hello: {value}</div>
}
class Wrapper extends Component<any, any>{
render() {
return <Demo />
}
}
render((<Wrapper />));
The issue is Ink is getting pegged to a version of react-reconciler that doesn't support hooks yet. I was able to get it working by using yarn and adding these resolutions to my package.json:
"resolutions": {
"react-reconciler": "0.18.0-alpha.2",
"scheduler": "0.12.0-alpha.3"
}
Those alpha versions have the hooks code. Then you can import the hooks and use them. I converted the README:
import React, { useState, useEffect } from 'react';
import { render, Color } from 'ink';
const Counter = () => {
const [i, setI] = useState(0);
useEffect(() => {
const timer = setInterval(() => setI(i + 1), 100);
return () => clearInterval(timer);
});
return <Color green>{i} tests passed</Color>;
};
render(<Counter />);
Doesn't work for me, when I am doing yarn install it says
warning Resolution field "[email protected]" is incompatible with requested version "scheduler@^0.11.0-alpha.0"
And still getting the invariant violation.
Can you please share the complete package.json please? and .babelrc?
Which version of react are you using?
It looks like your "dependencies" field has a version of scheduler, which it shouldn't. You should just use the "resolutions" field. Also, I tested this prior to the latest hooks version, so I'm not sure what version those should be now.
This is what I currently have, it installs fine but it still gives the invariant error after running it.
package.json
"dependencies": {
"@babel/preset-env": "^7.2.3",
"@babel/preset-react": "^7.0.0",
"@types/react": "^16.7.18",
"config": "2.0.1",
"ink": "^0.5.1",
"inversify": "4.13.0",
"lodash": "4.17.10",
"react": "16.7.0-alpha.2",
"react-dom": "16.7.0-alpha.2",
"reflect-metadata": "0.1.12",
"tsconfig-paths": "^3.7.0"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.2.2",
"@types/node": "10.3.6",
"babel-plugin-transform-react-jsx": "^6.24.1",
"jest": "22.4.0",
"mockshot": "0.2.6",
"nodemon": "1.17.5",
"rimraf": "2.6.2",
"ts-jest": "23.10.5",
"tslint": "5.11.0",
"typescript": "2.9.2",
"webpack": "4.28.2",
"webpack-cli": "3.1.2"
},
"resolutions": {
"react-reconciler": "0.18.0-alpha.2",
"scheduler": "0.12.0-alpha.3"
},
.babelrc
{
"presets": [["@babel/preset-env",
{
"useBuiltIns": "entry"
}]],
"plugins": [
[
"transform-react-jsx",
{
"pragma": "h"
}
]
]
}
This is what comes out of the code you showed:
"use strict";
var _react = _interopRequireWildcard(require("react"));
var _ink = require("ink");
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
var Counter = function Counter() {
var _useState = (0, _react.useState)(0),
_useState2 = _slicedToArray(_useState, 2),
i = _useState2[0],
setI = _useState2[1];
(0, _react.useEffect)(function () {
var timer = setInterval(function () {
return setI(i + 1);
}, 100);
return function () {
return clearInterval(timer);
};
});
return (0, _ink.h)(_ink.Color, {
green: true
}, i, " tests passed");
};
(0, _ink.render)((0, _ink.h)(Counter, null));
And the error:
/test-ink/node_modules/react/cjs/react.development.js:135
throw error;
^
Invariant Violation: Hooks can only be called inside the body of a function component.
at invariant (/test-ink/node_modules/react/cjs/react.development.js:128:15)
at resolveDispatcher (/test-ink/node_modules/react/cjs/react.development.js:1476:28)
at useState (/test-ink/node_modules/react/cjs/react.development.js:1499:20)
at VNode.Counter [as component] (/test-ink/test.js:18:39)
at mount (/test-ink/node_modules/ink/lib/diff.js:38:26)
at diff (/test-ink/node_modules/ink/lib/diff.js:136:3)
at build (/test-ink/node_modules/ink/lib/renderer.js:10:9)
at Renderer.update (/test-ink/node_modules/ink/lib/renderer.js:25:20)
at exports.render (/test-ink/node_modules/ink/index.js:61:14)
at Object.<anonymous> (/test-ink/test.js:36:17)
This is what I had when it worked:
{
"name": "test-cli-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ink": "2.0.0-10",
"react": "16.7.0-alpha.2"
},
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/node": "^7.2.2",
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.1.0",
"@types/chalk": "^2.2.0",
"@types/react": "^16.7.18",
"prettier": "^1.15.3",
"typescript": "^3.2.2"
},
"resolutions": {
"react-reconciler": "0.18.0-alpha.2",
"scheduler": "0.12.0-alpha.3"
}
}
{
"presets": ["@babel/preset-react", "@babel/preset-typescript"],
"plugins": ["@babel/plugin-transform-modules-commonjs"]
}
I know they published a new alpha version, so I don't know if that changes things, but it did work at the time.
Oh, wait, you're using ink's h, rather than React.createElement. That's your issue.
I thought h pragma is required. What is the correct setup then?
Ohhh my mistake, here's the issue: the current version of Ink is a custom implementation, so it's not tied to React in particular so much as it's inspired by it. The next version (2.0, as seen on the next branch) uses React directly, which is what you'll need to make hooks work. Since hooks are coupled to React, you can't use hooks with the current version of Ink at all.
If you switch to the next branch on GitHub, you can see the install & usage instructions. You'll also notice in my pkg.json that I'm using that version of Ink in my hooks-working test project.
I tried with the latest alphas as the following:
{
"dependencies": {
"ink": "2.0.0-10",
"react": "16.8.0-alpha.1",
"react-dom": "16.8.0-alpha.1"
},
"devDependencies": {
"@types/react": "^16.7.21",
},
"resolutions": {
"react-reconciler": "0.19.0-alpha.1",
"scheduler": "0.13.0-alpha.1"
}
}
Keep in mind that you have to use Yarn in order for the resolutions field to work. I initially tried with npm and couldn't get it to install the proper version of react-reconciler and scheduler. After using yarn, it worked with the following example:
import { render, Text } from 'ink';
import React, { useEffect, useState } from 'react';
const MyComponent = () => {
const [state, set] = useState(0);
useEffect(() => {
// Give it some time before updating to re-render with the new
// state later.
const id = setTimeout(() => {
set(1);
}, 250);
return () => {
clearTimeout(id);
};
});
return <Text>This {state}</Text>;
};
render(<MyComponent />);
Thanks everyone for pitching in on this issue! If I'm not mistaken, release of React Hooks is around the corner and they should release stable versions of their packages, which I will be able to safely upgrade Ink to. Afterwards hooks will be usable with Ink.
I have noticed that hooks doesn't quite work the way you're used to as in other environments. This is probably due to the nature of how the lifetime of a program works in command-line (or TTY).
For example, I tried to port ink-text-input using hooks and can't get input to work properly. I have tried with useEffect and process.on('keypress') as well as process.stdin.resume().
Without using resume to actively listen for user input, it will run, but the handler won't be called, even though the listener for the 'keypress' event is registered in useEffect like so:
useEffect(() => {
// Using resume will ask for user input and "pause" the program further and won't
// continue to render until that is done.
// process.stdin.resume();
process.stdin.on('keypress', handleKeyPress);
return () => {
process.stdin.removeListener('keypress', handleKeyPress);
};
}, []);
};
The useEffect hook is run once every render, but will only run the handler once as I pass an empty array [], so that we only register the listeners once and not every render (e.g. when value is updated from the outside).
The program will just run and exit, as there's nothing that tells it to stop and wait for user input before continuing.
I also tried to use the StdinContext with useContext in place of process.stdin, but that had the same result.
Using stdinContext.setRawMode will somehow render the component and seem to listen for user input, as the program keeps running but the handler passed to keypress isn't being executed. This doesn't happen if you use process.stdin.setRawMode directly, though.
Here's the code, using the StdinContext/useContext.
const TextInput = ({
focus,
value,
placeholder,
onChange,
onSubmit
}) => {
const stdinContext = useContext(StdinContext);
function handleKeyPress(ch, key) {
if (!focus) {
return;
}
if (hasAnsi(key.sequence)) {
return;
}
if (key.name === 'return') {
if (onSubmit) {
onSubmit(value);
}
}
if (key.name === 'backspace') {
if (onChange) {
onChange(value.slice(0, -1));
}
return;
}
if (
key.name === 'space' ||
(key.sequence === ch && /^.*$/.test(ch) && !key.ctrl)
) {
if (onChange) {
onChange(value + ch);
}
}
}
useEffect(() => {
stdinContext.setRawMode(true);
stdinContext.stdin.on('keypress', handleKeyPress);
return () => {
stdinContext.setRawMode(false);
stdinContext.stdin.removeListener('keypress', handleKeyPress);
};
}, []);
const hasValue = value.length > 0;
return <Color dim={!hasValue}>{hasValue ? value : placeholder}</Color>;
};
So I just investigated the keypress event thing, and I got it sorted out by using readline.emitKeypressEvents to emit keypress events for process.stdin and it seemed to work.
Yeah, older versions of scheduler that are used by useEffect, when run in not-a-browser, will just set a timeout that expires on infinity. React _will_ force-execute all pending useEffects before doing anything else that is new, but until then it'll just be pending forever.
Current scheduler (and the one that will be used by 16.8.0) will always use timeouts of 0 and merely order things according to priority.
@Jessidhia I think this is related to Ink not implementing schedulePassiveEffects or cancelPassiveEffects in its reconciler. With the now-released version of Hooks, I'm getting errors related to that function being missing. Anyone else play with this yet?
A very bare implementation would be setTimeout / clearTimeout; probably the best if there is no way to know when/if the terminal emulator is done updating the screen. Alternatively, just reexport unstable_scheduleCallback / unstable_cancelCallback from scheduler, which is what ReactDOM does (if scheduler can't find the DOM APIs it wants it falls back to _ => setTimeout(_, 0), but still orders callbacks according to priority).
It looks like this was a last-minute (good!) change in https://github.com/facebook/react/pull/14757
Cool, I'm working on a CLI tool with Ink@next so I'll PR this upstream. Ink also would need to bump some dep versions to get it working with Hooks again.
Now that hooks are officially released, I will look into how to make Ink work with them :) Thanks everyone for helpful input!
And of course, if somebody's already solved this problem, PRs are always welcome!
This should be fixed now 鉁岋笍 See #124
Hooks are working now thanks to @zkat!
Most helpful comment
Now that hooks are officially released, I will look into how to make Ink work with them :) Thanks everyone for helpful input!
And of course, if somebody's already solved this problem, PRs are always welcome!