Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using react hooks with ink: Invariant Violation #108

Closed
shlomokraus opened this issue Nov 21, 2018 · 20 comments
Closed

Using react hooks with ink: Invariant Violation #108

shlomokraus opened this issue Nov 21, 2018 · 20 comments

Comments

@shlomokraus
Copy link

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 />));
@shlomokraus shlomokraus changed the title Using react hooks with ink Using react hooks with ink: Invariant Violation Nov 21, 2018
@mAAdhaTTah
Copy link
Contributor

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 />);

@shlomokraus
Copy link
Author

shlomokraus commented Jan 15, 2019

Doesn't work for me, when I am doing yarn install it says
warning Resolution field "scheduler@0.12.0-alpha.3" 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?

@shlomokraus
Copy link
Author

Which version of react are you using?

@mAAdhaTTah
Copy link
Contributor

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.

@shlomokraus
Copy link
Author

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)

@mAAdhaTTah
Copy link
Contributor

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.

@mAAdhaTTah
Copy link
Contributor

Oh, wait, you're using ink's h, rather than React.createElement. That's your issue.

@shlomokraus
Copy link
Author

I thought h pragma is required. What is the correct setup then?

@mAAdhaTTah
Copy link
Contributor

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.

@fnky
Copy link

fnky commented Jan 26, 2019

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 />);

@vadimdemedes
Copy link
Owner

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.

@fnky
Copy link

fnky commented Jan 27, 2019

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>;
};

@fnky
Copy link

fnky commented Jan 27, 2019

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.

@Jessidhia
Copy link

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.

@mAAdhaTTah
Copy link
Contributor

@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?

@Jessidhia
Copy link

Jessidhia commented Feb 12, 2019

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 facebook/react#14757

@mAAdhaTTah
Copy link
Contributor

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.

@vadimdemedes
Copy link
Owner

vadimdemedes commented Feb 15, 2019

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!

@zkat
Copy link
Contributor

zkat commented Feb 16, 2019

This should be fixed now ✌️ See #124

@vadimdemedes
Copy link
Owner

Hooks are working now thanks to @zkat!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants