Javier Manzano
Javier Manzano

Reputation: 4821

Yarn workspaces with React and React Native

I'm working on creating a yarn workspace to share code between React and React Native (upcoming blog post once it's completely done!).

The most important part for us is sharing business logic between both platforms. In this case we are using react-query for network requests.

We've created a "Render prop" component for that

import { useAllDevices } from "../../queries/devices";

export interface DeviceListProps {
    devices: any[];
    isLoading: boolean,
    onItemClick?: () => void;
}

export interface DeviceItemListProps {
    name: string;
    onItemClick?: () => void;
}

export const DeviceListContainer = ({ render }: { render: any }) => {
    const { data, isLoading } = useAllDevices();
    return (
        <>
            {render({ devices: data?.devices, isLoading })}
        </>
    )
}

where useAllDevices is something like this:

export const useAllDevices = () => useQuery('useAllDevices', async () => {
    const devicesResponse = await get('/todos');
    return {
        devices: devicesResponse.data,
    };
});

In web, it works like a charm, but I'm getting an error for mobile app. It seems like the problem is with react-query itself because once I put this:

const queryClient = new QueryClient();

const App = () => {
  const isDarkMode = false;

  const backgroundStyle = {
    backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
  };

  return (
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={THEME}>
        <SafeAreaView style={backgroundStyle}>
          <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
          <ScrollView
            contentInsetAdjustmentBehavior="automatic"
            style={backgroundStyle}>
            <Header />
            <Button styleType="primary">hey</Button>
          </ScrollView>
        </SafeAreaView>
      </ThemeProvider>
    </QueryClientProvider>
  );
};

I get this error

React Native Error

It is working properly and with no problems for React web version

my package.json on the App module is this

{
  "name": "@sharecode/app",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest --updateSnapshot",
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx"
  },
  "dependencies": {
    "react": "17.0.2",
    "react-native": "0.67.3",
    "react-native-gesture-handler": "^2.3.0",
    "styled-components": "^5.3.3"
  },
  "devDependencies": {
    "@babel/core": "^7.12.9",
    "@babel/runtime": "^7.12.5",
    "@react-native-community/eslint-config": "^2.0.0",
    "@sharecode/common": "1.0.0",
    "@testing-library/jest-native": "^4.0.4",
    "@testing-library/react-native": "^9.0.0",
    "@types/jest": "^27.4.1",
    "@types/react-native": "^0.66.15",
    "@types/react-test-renderer": "^17.0.1",
    "@types/styled-components-react-native": "^5.1.3",
    "@typescript-eslint/eslint-plugin": "^5.7.0",
    "@typescript-eslint/parser": "^5.7.0",
    "babel-jest": "^26.6.3",
    "eslint": "^7.14.0",
    "get-yarn-workspaces": "^1.0.2",
    "jest": "^26.6.3",
    "metro-config": "^0.56.0",
    "metro-react-native-babel-preset": "^0.66.2",
    "nock": "^13.2.4",
    "react-test-renderer": "17.0.2",
    "ts-jest": "^27.1.3",
    "typescript": "^4.4.4"
  },
  "workspaces": {
    "nohoist": [
      "react-native",
      "react-native/**",
      "react",
      "react/**",
      "react-query",
      "react-query/**"
    ]
  },
  "resolutions": {
    "@types/react": "^17"
  },
  "jest": {
    "preset": "react-native",
    "setupFilesAfterEnv": [
      "@testing-library/jest-native/extend-expect"
    ],
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx",
      "json",
      "node"
    ]
  }
}

Main package

{
  "name": "@sharecode/common",
  "version": "1.0.0",
  "main": "index.ts",
  "license": "MIT",
  "dependencies": {
    "axios": "^0.26.0",
    "react-query": "^3.34.16",
    "styled-components": "^5.3.3"
  },
  "devDependencies": {
    "@types/styled-components": "^5.1.24"
  }
}

And web package (working perfectly)

{
  "name": "@sharecode/web",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.2",
    "@testing-library/react": "^12.1.3",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.4.1",
    "@types/node": "^16.11.26",
    "@types/react": "^17.0.39",
    "@types/react-dom": "^17.0.13",
    "react": "17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "^5.0.0",
    "typescript": "^4.6.2",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "eslint-config-react-app": "^7.0.0",
    "react-app-rewired": "^2.2.1"
  }
}

The error seems to be pretty straightforward but I cannot see what's going on

 ERROR  Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

This error is located at:
    in QueryClientProvider (at App.tsx:31)
    in App (at renderApplication.js:50)
    in RCTView (at View.js:32)
    in View (at AppContainer.js:92)
    in RCTView (at View.js:32)
    in View (at AppContainer.js:119)
    in AppContainer (at renderApplication.js:43)
    in NuoDoor(RootComponent) (at renderApplication.js:60)
✨  Done in 318.43s.

Also, here is the result of `yarn why react``

javiermanzano@Javiers-MBP app % yarn why react
yarn why v1.22.17
[1/4] 🤔  Why do we have the module "react"...?
[2/4] 🚚  Initialising dependency graph...
[3/4] 🔍  Finding dependency...
[4/4] 🚡  Calculating file sizes...
=> Found "@sharecode/app#[email protected]"
info Reasons this module exists
   - "_project_#@sharecode#app" depends on it
   - in the nohoist list ["/_project_/@sharecode/app/react-native","/_project_/@sharecode/app/react-native/**","/_project_/@sharecode/app/react","/_project_/@sharecode/app/react/**","/_project_/@sharecode/app/react-query","/_project_/@sharecode/app/react-query/**"]
info Disk size without dependencies: "356KB"
info Disk size with unique dependencies: "404KB"
info Disk size with transitive dependencies: "432KB"
info Number of shared dependencies: 3
=> Found "[email protected]"
info Reasons this module exists
   - "_project_#@sharecode#web" depends on it
   - Hoisted from "_project_#@sharecode#web#react"
info Disk size without dependencies: "356KB"
info Disk size with unique dependencies: "404KB"
info Disk size with transitive dependencies: "432KB"
info Number of shared dependencies: 3
✨  Done in 1.17s.

I hope I explained the problem! Any help is appreciated :)

Thank you!

Upvotes: 7

Views: 2839

Answers (1)

Luigi Minardi
Luigi Minardi

Reputation: 353

I don't think your problem is related to having mismatching versions of React and the renderer, so we end with 2 options.

Option 1:

3. You might have more than one copy of React in the same app

I'll start covering the 3rd since is the most probable, in this case your program is detecting two reacts.

In your yarn why react you got a react from "_project_#@sharecode#app" and "_project_#@sharecode#web" Hoisted from "_project_#@sharecode#web#react", remember this Hoisted info cuz it's important for solve your problem.

Looking at your package.json you do have the react installed on both. And looking at your workspaces nohoist in "@sharecode/app" you say to don't hoist the react package, but as I said before it is be hoisting!.

So what is the problem?

Well, as I understood and are illustrated at the nohoist documentation you need to put the nohoist config at the package.json in your root "monorepo" (In your case the @sharecode/common).

It will look like this:

{
  "name": "@sharecode/common",
  ...,
  "devDependencies": {
     ...
  },
  "workspaces": {
     "packages": ["packages/*"],
     "nohoist": [
       ...,
       "**/react",
       "**/react/**",
       ...
    ]
  }
}

Than, with your package.json working as expected when you run yarn why react you should get something like this:

...
=> Found "@sharecode/app#[email protected]"
info Reasons this module exists
   - "_project_#@sharecode#app" depends on it
   - in the nohoist list ["/_project_/**/react-native","/_project_/**/react-native/**","/_project_/**/react","/_project_/**/react/**","/_project_/**/react-query","/_project_/**/react-query/**"]
...

Maybe a found in the "_project_#@sharecode#web" too, with other nohoist probably. I didn't checked it myself, but with this correction said above you should be with everything running fine. You can check this and this for further explanation on how it works. But I'm pretty sure that the problem is the nohoist not being in the "root monorepo".

P.S.: I tryed to find what is the reason for using "packages": ["packages/*"], and didn't find out. But I'm deducing that it is to say that you're hoisting everything that aren't in nohoist.

Option 2:

2. You might be breaking the Rules of Hooks

Well, if you couldn't solve your problem with the first option, your problem is related to a hook.

I don't know if it's this your problem, but you're calling a bool in a const and probably trying to change it in some place. Can't you refactor to use a useState(false)?

Also check if you'd called some hook inside loops, conditions or nested funcitons in some of your children, probably the QueryClientProvider if I don't miss understood.

Searching for the Rules of Hooks I found this:

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)

Upvotes: 2

Related Questions