Skip to content

Creating Plugins in React

I am learning React. I have experience with Vue.js. Vue.js has the concept of plugins which allow you to inject functionality across pieces of your app. The pieces may be components, state management, router, etc. A common need for plugins is translation or logging. My question is, does React have a concept like plugins or services? If so, what is it?

I do not see anything similar to plugins in the React docs. Several blog posts I’ve reviewed, do not seem to use plugin in the same way. How can one provide programmatically accessible functionality that is available globally throughout a React app?

Answer

A common pattern is to use a React context for this. A fairly similar example to the Vue.js plugin documentation you’ve linked, would be something like this:

const I18n = React.createContext();

function useTranslate() {
  const i18n = React.useContext(I18n);
  
  return React.useCallback((key) => {
    return key.split('.').reduce((o, i) => {
      if (o) return o[i];
    }, i18n);
  }, [i18n]);
}

function App() {
  const translate = useTranslate();

  return (
    <h1>{translate("greetings.hello")}</h1>
  );
}

// app initialization
const FR = {
  greetings: {
    hello: 'Bonjour!'
  }
};

const root = ReactDOM.createRoot(document.querySelector("#root"));
root.render(
  <I18n.Provider value={FR}>
    <App />
  </I18n.Provider>
);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="root"></div>

The above is fairly limiting since a user would not be able to select their own language. A more complex example example would be where a user can select their language. Here is some inspiration on how you could implement this:

// Create a component that wraps it't children in a context.
function I18n({ initial, dicts, children }) {
  const [lang, setLang] = React.useState(initial);
  const dict = dicts[lang];
  
  const contextValue = React.useMemo(
    () => ({ lang, setLang, dict, dicts }),
    [lang, setLang, dict, dicts]
  );

  return (
    <I18n.Context.Provider
      value={contextValue}
      children={children}
    />
  );
}

// Create the actual React context.
I18n.Context = React.createContext();

// Provide custom hooks to simplify working with the data.
// You could for example use a simple templating engine.
I18n.useTranslate = function () {
  const { dict } = React.useContext(I18n.Context);
  
  return React.useCallback((key, view) => {
    const template = key.split(".").reduce((dict, key) => dict[key], dict);
    return Mustache.render(template, view);
  }, [dict]);
};

// Provide custom components to allow a user to interact with your context.
I18n.LangSelect = function (props) {
  const { lang, setLang, dicts } = React.useContext(I18n.Context);
  
  const changeLang = React.useCallback((event) => {
    setLang(event.target.value);
  }, [setLang]);

  return (
    <select {...props} value={lang} onChange={changeLang}>
      {Object.entries(dicts).map(([key, dict]) => (
        <option key={key} value={key}>
          {dict.langSelect.label}
        </option>
      ))}
    </select>
  );
};

// All the above could be located in a `plugin/i18n.jsx` file, or
// wherever you would like to store it. The code below would be
// the usage of this "plugin".

function App() {
  // Wrap the parts of the app you want to apply the context to.
  return (
    <div>
      <I18n.LangSelect />
      <Greet />
    </div>
  );
}

// Then in a child component use eiter `useContext` or the custom hook
// we created earlier.
function Greet() {
  const i18n = React.useContext(I18n.Context); // access the raw context data
  const translate = I18n.useTranslate(); // or use your custom hook(s)

  return (
    <h1>
      {i18n.dict.greetings.hello}{" "}
      {translate("greetings.introduce", { name: "John Doe" })}
    </h1>
  );
}

// app initialization

// The dictionaries could be imported from a separate file.
const DICTS = {
  EN: {
    langSelect: {
      label: "English",
    },
    greetings: {
      hello: "Hello!",
      introduce: "I'm {{name}}.",
    },
  },
  FR: {
    langSelect: {
      label: "Fran├žais",
    },
    greetings: {
      hello: 'Bonjour!',
      introduce: "Je m'appelle {{name}}.",
    },
  }
};

const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(
  <I18n initial="EN" dicts={DICTS}>
    <App />
  </I18n>
);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/mustache@4/mustache.js"></script>
<div id="root"></div>

Note that you don’t necessarily need a React context. If you have a library function or something similar, you can just import it into your file and use it.