Generate RTL CSS file in create-react-app and switch between them based on change in state

3.9k Views Asked by At

I'm using create-react-app for a multi-language project. I want to use some library like "cssJanus" or "rtlcss" to convert the Sass generated CSS file into a separate file and then use that newly generated file when I switch to another language.

Here's how my index.js looks like ...

import React from "react";
import ReactDOM from "react-dom";
import * as serviceWorker from "./serviceWorker";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider } from "react-redux";
import App from "./App";
import { configureStore } from "./store/configureStore";

const store = configureStore();

ReactDOM.render(
    <Provider store={store}>
        <Router>
            <App />
        </Router>
    </Provider>,
    document.getElementById("root")
);

serviceWorker.unregister();

And here's how my "App.js" looks like ...

import React, { Component } from "react";
import "./App.scss";
import { Route, Switch } from "react-router-dom";
import SignIn from "./features/signin/SignIn";

class App extends Component {
    render() {
        return (
            <>
                <Switch>
                    <Route path="/" exact component={SignIn} />
                </Switch>
            </>
        );
    }
}

export default App;

As you can see I'm using "./App.scss" file that simply have a bunch of @import statements to another ".scss" files in the "./src/css/" directory ...

/* autoprefixer grid: on */
@import "css/reset";
@import "css/variables";
@import "css/global";

I need your advice on how to do that. How to convert the generated CSS from App.scss to RTL into their own .css file and switch between them and the original generated CSS based on a change in the global state.

I searched a lot for something like this but with no luck.

Or if you have a better approach I'm all ears.

3

There are 3 best solutions below

1
On

Looking around there is a library called react-with-direction from airbnb that provides a DirectionProvider - component you could wrap your components in based on the language. Hope that helps.

1
On

If you use flexbox and css grid they have RTL support built in. Then use CSS Logical Properties for margin, padding, border, etc. If that is not enough, then you can use [dir="rtl"] .your-class as a fallback.

Now you don't have two separate css files to maintain.

Here is a cross browser margin-right example.

-webkit-margin-end: 25px;
margin-inline-end: 25px;
@supports (not (-webkit-margin-end: 0)) and (not (margin-inline-end: 0)) {
    margin-right: 25px;
}

You could wrap that up into a mixin for easier use across your app.

3
On

Here is a simple solution that requires ejecting and adding a lightweight webpack-rtl-plugin.

After running

npx create-react-app react-rtl 
cd react-rtl
yarn eject
yarn add -D webpack-rtl-plugin @babel/plugin-transform-react-jsx-source

Go to config/webpack.config.js and make some tweaks:

// import the plugin
const WebpackRTLPlugin = require('webpack-rtl-plugin')

// ...

module: { ... }
plugins: [
   // ...,
   // use the plugin
   new WebpackRTLPlugin({ diffOnly: true })
].filter(Boolean),
// ...

On this stage, if you run yarn build and look up build/static/css folder, you should hopefully see additional .rtl.css file that contains your rtl styles. Then we need to tell webpack to use MiniCssExtractPlugin.loader for development as well so it will serve styles through link tags instead of inline styles:

// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
  const loaders = [
    isEnvDevelopment && { loader: MiniCssExtractPlugin.loader }, // <-- use this
    // isEnvDevelopment && require.resolve('style-loader'), <-- instead of this 

and don't forget the plugin, lol:

module: { ... }
plugins: [
   // ...,

   // isEnvProduction &&      <-- comment this out
   new MiniCssExtractPlugin({
     // Options similar to the same options in webpackOptions.output
     // both options are optional
     filename: 'static/css/[name].[contenthash:8].css',
     chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
   }),

   // ...
].filter(Boolean),

And from here you can finally grab your default stylesheet href and use to insert rtl styles. Here's how you could implement it:

class RtlCssBundleService {
  constructor() {
    this.rtlApplied = false
    this.rtlStyles = [];
    this.ltrStyles = Array.from(
      document.querySelectorAll('link[rel="stylesheet"]')
    )
  }

  insert = () => {
    if (this.rtlApplied) { return }

    this.rtlApplied = true

    if (this.rtlStyles.length) {
      return this.rtlStyles.forEach(style => {
        document.body.appendChild(style)
      })
    }

    this.rtlStyles = this.ltrStyles.map(styleSheet => {
      const link = document.createElement("link")
      link.href = styleSheet.href.replace(/\.css$/, '.rtl.css')
      link.rel = "stylesheet"
      document.body.appendChild(link)
      return link
    })
  }

  detach = () => {
    this.rtlApplied = false
    this.rtlStyles.forEach(style => {
      document.body.removeChild(style)
    })
  }

  toggle = () => {
    return this.rtlApplied
      ? this.detach()
      : this.insert()
  }
}

const rtlStyles = new RtlCssBundleService()

export default rtlStyles

Then use this from any of your components. So anyway, I'm sure I've missed something and maybe that is a terrible approach, but it seems to work and here is the demo