How to use Apollo/Graphql hooks within external React.js component library - reactjs

I have 3 React.js projects and I'm trying to consolidate the reused components across all 3 projects into one shared component library.
This component library is hosted on Github and imported into all 3 projects through the package.json file like this:
"components": "github:myusername/components"`
I am then able to import the components from the library into each project like such:
import { Form } from "components"
but when the component being imported (in this case a Form) uses an Apollo/Graphql hook (useQuery, or useMutation) I get this error:
Uncaught Invariant Violation: Could not find "client" in the context or passed in as an option. Wrap the root component in an <ApolloProvider>, or pass an ApolloClient instance in via options.
I've found countless questions about this same error, but the solutions are either already in place or irrelevant. Here is what I've found so far:
Wrap the App with the <ApolloProvider client={client}> tag (this is already there in the 3 projects - but not in the "components" library)
use the #apollo/client package instead of the #apollo/react-hooks - link (already using that package)

The solution that worked for me was to define #apollo/client as a peer dependency in the root package.json of my shared lirbary (I'm using lerna).
It didn't work for me to just link the package with yarn link but I had to publish it on npm.

I looked into this a little more given Kn3cht's information that the shared component works if published to NPM and found a beautiful solution. There is a utility called yalc that works perfectly. The problem with npm/yarn link is that symlinked dependencies are not deduped. To use, install yalc globally (eg, npm install yalc --global or yarn global add yalc).
Let's say the component to be shared is called shared-component.
In the root folder of shared_component, type 'yalc push'.
In the root folder of component that will import the shared_component, type 'yalc link shared-component'
That's it. I can't believe this utility is not more widely known. I believe you still need to designate react and apollo dependencies as peerDependencies in package.json.
As for Lerna, there is a small wiring needed to have updates automatically pushed whenever the shared-component changes. Essentially, install watch at the root of the mono repo as a dev dependency, then add scripts to the root and all component package.json files that you want to have automatically pushed. For example, the target I create is called "watch:push". In the root package.json:
"scripts": {
"watch:push": "lerna run watch:push"
}
For each component that I want to share I add this target to its package.json:
"scripts": {
"push": "yalc push",
"watch:push": "watch 'yarn push' ./dist",
}
Here you can manually push your shared-component via yarn push, or you can have the shared-component pushed whenever the contents of the build folder have changed (in my case the folder is dist).
So to make this fully automatic, you need to run run a command separately that executes a build whenever source code changes.

Related

React conflict when consuming a local react library

I have a Parent React App that consumes a module library which is also a React app. I used to have react and react-dom installed in the parent project and have them added as a peerDependencies in package.json in the modules library. But, now I need to install the two packages as devDependencies in the modules library.
This resulted in Invalid Hook Error, which I narrowed down to the third point:
you might have more than one copy of React in the same app
Parent App's package.json:
{
// Other Deps..
"modules-lib": "file:../libs/modules-lib/dist",
}
I've tried a couple of solutions from this issue, but things didn't work. I also tried linking react and react-dom using npm link from our modules library to the parent app and it actually worked.
I was wondering that why does the consumer of the modules library looks into its node_modules folder and why the two packages are being listed as dependencies and not devDependencies in the final build folder in package.json

Trying to create a shareable React component - but failing to import it

I'm trying to share a React component I've created through a local hosted npm repo.
To do that I created the React component with typescript, transpiled it to js and published the resulting code to the repo. But when I install this package in an existing project (a basic create-react-app project with typescript) and try to use that component - My app tried to compile it for a few minutes and I fail to load that component. Sometimes if I wait a few minutes I see this error - although the component was tested and works:
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:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
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.'
I've copied the same component to be embedded in the app and not installed by npm - it worked. I tried to strip the component to the bare minimum - it still takes a long time.
The steps to reproduce are easy:
I've shared the component in github:
https://github.com/ymoran00/example-stackoverflow-react
To build it you need to run npm install and then npm run build.
the result will be generated in the lib folder.
You can then go into the lib folder and run:
npm link
Then create a new typescript create-react-app project:
npx create-react-app my-app --template typescript
Go inside it and run:
npm link login-component
This will install the linked package.
Now go to App.tsx and import the package:
import LoginContainer from 'login-component/LoginContainer';
And use it in the App:
<LoginContainer onLogin={()=> {alert('success')}}/>
Run the app with npm start.
The App will open the browser - but nothing will load. It's kind of stuck on build or whatever - I don't know what happens there. If you'll take a look at the component you'll see it's quite a basic one with Material-UI.
The first place that I looked was your package.json file because it most likely that you are dealing with reason #1:
You might have mismatching versions of React and the renderer (such as React DOM)
I see that you are including react and react-dom as dependencies for your component. You should move these from dependencies to peerDepenedencies. You likely want to move #material-ui/core and #material-ui/icons to peerDependencies as well. Right now React is being bundled with your component and your component uses its own React version rather than the one in your app. You want to make it so that projects that use your component are expected to include React on their own.
For more information about why you should use peerDependencies, see this question:
What's the difference between dependencies, devDependencies and peerDependencies in npm package.json file?
It seems that the main problem I had in the process is using npm link.
It causes problems when combined with hooks - that's why I get this hooks error.
See also in this thread:
https://github.com/facebook/react/issues/13991
So instead of using npm link I've started using npm-sync and it seems to solve the problem.

Getting error when using FormattedMessage inside a module: Error: [React Intl] Could not find required `intl` object

I have a monorepo which exposes a TypeScript module, which is consumed & used by a React TypeScript project.
When the module inserts arbitrary React elements to the virtual DOM - everything works as expected, including when I try to use React Router (which was initially problematic but I was able to fix that).
However, when I try to use react-intl, via FormattedMessage, I get the error:
Error: [React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry.
Which is especially annoying as I see this printed in the console logs:
The above error occurred in the <Context.Consumer> component:
in FormattedMessage
in h2
in div
in Loading (at App.tsx:11)
in IntlProvider (at App.tsx:8)
in App (at src/index.tsx:9)
in StrictMode (at src/index.tsx:8)
(note the IntlProvider wrapping Loading - which is the element that uses FormattedMessage which can't find IntlProvider).
I imagine this is somehow related to versioning, or having 2 instances of React / React DOM / IntlProvider, but I have no idea to how solve this, and I have spent quite a lot of time trying everything I could think of.
For what it's worth, here's what I use:
TypeScript - for both module and project
Webpack to pack the module, where I declared React, ReactDOM and react-intl as externals and added them as peerDependencies rather than direct dependencies
create-react-app for the project
I was able to create a minimal repro repository, here's how to repro my issue:
<cd somewhere>
git clone https://github.com/chakaz/repro-repo .
cd repro-lib
npm install
npm run build:dev
cd ../project
npm install
npm run start
Anyone has any idea? Tons of thanks in advance!
With your above way in order to make it work, you have to delete node_modules in your repro-lib dir cause it will install dependencies in both dirs.
So in order to resolve problem of monorepo, I'd like to suggest you use yarn's workspace functionality as described carefully here: https://classic.yarnpkg.com/en/docs/workspaces/
To summary, it's a great functionality to help working with multiple workspaces by just only yarn install once.
Here are a few steps to make your repo working:
Put package.json at the root level of the project with following content:
{
"private": true,
"workspaces": ["project", "repro-lib"]
}
Go to project dir and replace following line in package.json:
"pf-common": "file:../repro-lib"
to
"pf-common": "1.0.0"
Finally, just go back to root top level install deps again:
yarn install
That's it! Now you can re-run your application to see how it works.
NOTE: In terms of having interest in monorepo, lerna is also great tool comes to help by providing great CLI.

Module not found error using Yarn 2 to link React components

I've created a repository which contains a React app (created with create-react-app) and a components directory which contains a simple Material UI button. The folder structure is:
/components
/react-app
Both directories are set up to use Yarn 2, and are not in a workspace (as I'm trying to simulate projects in separate directories and simplify my real world scenario).
I build the components:
$ cd ~/components && yarn build
I then Yarn link the components to the React app:
$ cd ~/react-app & yarn link ../components -r -p
This results in a modification to package.json file in the react-app directory:
{
"name": "react-app",
...
"resolutions": {
"components": "portal:../components"
}
}
My App.tsx file looks like this:
import './App.css';
import { Button } from 'components';
import React from 'react';
function App() {
return (
<Button>Test</Button>
);
}
export default App;
However, when I run the React app using yarn start I get the following error:
./src/App.tsx
Module not found: Your application tried to access components, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.
I'm not sure what I'm doing wrong. If I add an explicit reference to the components directory within dependencies (which I don't believe I should have to do because I've already linked it) such as:
"dependencies": {
"components": "portal:../components"
}
Then I get the error:
./src/App.tsx
Module not found: You attempted to import ~/react-app/.yarn/$$virtual/components-virtual-de9a8055ab/2/components which falls outside of the project src/ directory. Relative imports outside of src/ are not supported.
Surely, I don't have to eject the app and find a way to bypass this error?
EDIT: According to the Yarn documentation "Webpack 5 will support PnP natively, but if you use Webpack 4 you'll need to add the pnp-webpack-plugin plugin yourself". At the time of writing, the latest version of create-react-app relies on v3.4.1 of react-scripts which in turn relies on Webpack 4. I therefore ejected my app to inspect the Webpack configuration, and it appears that this plugin is already installed. It's therefore not a CRA/Webpack issue. I also upgraded my version of Node from v10.16.0 to v12.16.3 to no avail.
TLDR; Add the package as a dependency then modify your React setup to import files outside of the /src directory.
In my case, it doesn't look like yarn link is doing anything other than adding a resolutions entry in package.json, which according to the documentation is only used to specify the version to resolve. Perhaps my understanding of Yarn link is wrong (even though it held up in v1).
To fix the issue I had to add the reference to dependencies in package.json (even though I'd already run yarn link):
"dependencies": {
"components": "portal:../components"
}
This caused the aforementioned You attempted to import components which falls outside of the project src/ directory error. To resolve this we either need to eject the React app and disable the ModuleScopePlugin in Webpack (therefore allowing the import of files outside the /src folder), or use craco with custom configuration. I've created yarn-eject and craco branches to demonstrate both.
It's not a particularly elegant solution, and I'm hoping someone can post a better alternative. I switched to Yarn 2 so that I could utilise the "Improved Peer Dependency Links" feature (so that I'm only relying on one version of react across my applications and shared components packages). I'd rather not have to eject my React app or use custom configuration if possible.

How to import a React component I've made locally without node_modules?

I built a react component (my-shared-component) that I would like to use with other projects locally. I used rollup to bundle my component and the output is a dist folder.
In order to avoid publishing my component to npm yet, I used npm link. However, when I do npm link <my-shared-component> in my host component, the entire shared component folder is added inside my node_modules, including my component's node_modules library, source files, etc - This causes several bugs in my host app.
Obviously I don't want to do, if I had published my component to npm and then used npm -i my-shared-component, only my dist folder would've been installed.
I can I mimic this behavior locally using npm-link? I want to use the packaged version of my-shared-component.
Thank you!
I ended up solving the case by using npm pack inside my my-shared-component and then npm i ../.../full-path/my-shared-component-1.0.0.tgz inside my host app.
That way I only get the packed bundled version of my-shared-component without the node_modules and source code.
This does mean that I have to pack my shared component every time which is not ideal. If anyone has any better solution please let me know.
Thanks

Resources