I am having issues importing a JSON file depending on a process.env.WORLD
value in my React Typescript app. This is done in a .tsx
file defining a React Context, and no React components are used here.
The JSON file can be loaded without problems when we ignore the process.env
variable.
import data from '../main.json';
- TS file loading the JSON file:
src/contexts/world.tsx
- JSON file:
src/main.json
In my first attempt, I tried
let file;
if (process.env.WORLD == 'main') {
file = '../main.json';
} else {
file = '../test.json';
}
import data from file;
but got the error
Import in body of module; reorder to top import/first
Tried using dynamic import
const data = await import(
process.env.WORLD == 'main'
? '../main.json'
: '../test.json'
);
but my setup does not allow a top-level await
Module parse failed: Cannot use keyword 'await' outside an async function
Tried using require
instead of import
let file;
if (process.env.WORLD === 'main') {
file = '../main.json';
} else {
file = '../test.json';
}
const data = require(file);
but it almost works except that the file cannot be found
contexts sync:2 Uncaught Error: Cannot find module '../main.json'
Strangely, there is no problem using the same file path with import
as shown in the first example in this question.
Typescript target is ES2018
, and React 18 is used.
Why is the file not found when using require
, and how can we solve this problem?
CodePudding user response:
In a comment, you added information which implies that the external JSON data is being used as part of the value
of a ContextProvider
.
By fetching (or importing) the JSON data dynamically just before render, you can pass it via props to your context provider on the initial render, then memoize it in a ref
. Below is a working example in a Stack Overflow snippet which uses object URLs in place of your local file specifiers.
Note that there are many considerations involved in how to optimize external data loading, and your question does not provide much detail regarding your scenario, but the approach that works best for your app will depend on those details. In any case, what you've described in the question would be better handled by re-writing the contents of a single JSON file on disk (based on the environment variable) before building or running your app each time. This would need to be handled outside the React app by an external process (like an npm script, for example), and would allow you to avoid the scenario entirely, while also optimizing the build result with static imports.
<div id="root"></div><script src="https://unpkg.com/[email protected]/umd/react.development.js"></script><script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
// import ReactDOM from 'react-dom/client';
// import {
// default as React,
// createContext,
// StrictMode,
// useContext,
// useRef,
// type ReactNode,
// type ReactElement,
// } from 'react';
// This Stack Overflow snippet demo uses UMD modules
// instead of the commented import statments above
const {
createContext,
StrictMode,
useContext,
useRef,
} = React;
type JsonData = {
items: string[];
listTitle: string;
};
// An initial value is not required as long as this
// is not used outside the context provider:
const jsonContext = createContext(undefined as unknown as JsonData);
function JsonProvider ({children, data}: {
children: ReactNode;
data: JsonData;
}): ReactElement {
const ref = useRef(data);
return (
<jsonContext.Provider value={ref.current}>
{children}
</jsonContext.Provider>
);
}
function App (): ReactElement {
const {items, listTitle} = useContext(jsonContext);
return (
<div>
<h1>{listTitle}</h1>
<ul>
{items.map((item, index) => (<li key={index}>{item}</li>))}
</ul>
</div>
);
}
type JsonPrimitive = boolean | null | number | string;
type JsonSerializable = JsonPrimitive | JsonSerializable[] | { [key: string]: JsonSerializable };
function createJsonObjectUrl (serializable: JsonSerializable): string {
const json = JSON.stringify(serializable);
const blob = new Blob([json], {type: 'application/json'});
return URL.createObjectURL(blob);
}
async function inititalRender () {
// The Stack Overflow code snippet sandbox doesn't have Node's `process`,
// so we'll simulate it here:
const process = {
env: {
// Simulate that it could be either "main" or "test"
REACT_APP_JSON_FILE: Math.random() < 0.5 ? 'main' : 'test',
} as Partial<Record<string, string>>,
};
// In your app, this would simply be "../main.json" or "../test.json",
// but the Stack Overflow code snippet sandbox doesn't have access to
// your local files, so we'll simulate the specifier with an object URL
// (which can also be fetched/imported):
const specifier = process.env.REACT_APP_JSON_FILE === 'main'
? createJsonObjectUrl({
items: ['a', 'b', 'c'],
listTitle: 'main',
})
: createJsonObjectUrl({
items: ['one', 'two', 'three'],
listTitle: 'test',
});
// Use fetch to get the data (we're in an async function, so that's OK!)
const data = await (await fetch(specifier)).json();
// Or (in the future) dynamic import with an assertion:
// const {default: data} = await import(specifier, {assert: {type: 'json'}});
const reactRoot = ReactDOM.createRoot(document.getElementById('root')!);
reactRoot.render(
<StrictMode>
<JsonProvider {...{data}}>
<App />
</JsonProvider>
</StrictMode>
);
}
inititalRender();
</script>
CodePudding user response:
Could importing both files then choose the right file be a fit for you ?
import main from '../main.json';
import test from '../test.json';
let file;
if (process.env.WORLD == 'main') {
file = main;
} else {
file = test
}
or use react.lazy
:
const Questions = React.lazy(() => {
if (process.env.WORLD == 'main') {
import('../main.json'))
} else {
import('../test.json'))
}
}