Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Remix support #39765

Open
1 of 8 tasks
samuelsycamore opened this issue Nov 6, 2023 · 22 comments
Open
1 of 8 tasks

Improve Remix support #39765

samuelsycamore opened this issue Nov 6, 2023 · 22 comments
Assignees
Labels
dx Related to developers' experience umbrella For grouping multiple issues to provide a holistic view

Comments

@samuelsycamore
Copy link
Contributor

samuelsycamore commented Nov 6, 2023

I've started experimenting with using Material UI and Joy UI components with Remix and I've run into a few bugs and blockers along the way. Browsing the issues here, I see several open threads where folks are discussing the same problems I've bumped into, so I thought it might be helpful to create an umbrella issue to bring all those discussions to one centralized, up-to-date location where folks can track the progress.

Here are all of the open issues I could find—some are overlapping, others are probably pretty outdated and possibly no longer relevant but I haven't dug too deeply into each one:

Open

Problems

Here are the biggest things I see after reviewing these issues:

Potential solutions

Examples and docs

There are several things we could do to improve the documentation around Remix + MUI!

If there are any other known issues or requests to improve the DX, please share them here!

@samuelsycamore samuelsycamore added umbrella For grouping multiple issues to provide a holistic view dx Related to developers' experience labels Nov 6, 2023
@samuelsycamore samuelsycamore self-assigned this Nov 6, 2023
@arunmmanoharan
Copy link

@samuelsycamore I have MUI working with renderToPipeableStream using this sample:

https://github.com/remix-run/examples/blob/main/chakra-ui/app/entry.server.tsx

@yehudamakarov
Copy link

yehudamakarov commented Nov 24, 2023

I have this issue where if I import like so:
import { useDemoData } from "@mui/x-data-grid-generator";
I get

Uncaught TypeError: Cannot read properties of undefined (reading 'A400')
    at theme.ts:25:17
(anonymous) @ theme.ts:25

The same problem came up for me before and I solved it by changing

import { Alert, Grid } from "@mui/material";

to

import Alert from "@mui/material/Alert";
import Grid from "@mui/material/Grid";

Basically to reproduce,

  1. use the remix material UI example
  2. make a new route
  3. paste in an example from Material UI datagrid that uses useDemoData

@rahad06
Copy link

rahad06 commented Dec 8, 2023

the provided remix template is old and not compatible, packages and layout is obsolete. please provide an up-to-date remix admin template for MUI with internationalization

@mbarni99
Copy link

mbarni99 commented Dec 17, 2023

@samuelsycamore I have MUI working with renderToPipeableStream using this sample:

https://github.com/remix-run/examples/blob/main/chakra-ui/app/entry.server.tsx

Hello,

I have just tried this one out and it may be outdated as I got the following error:
Module '"@remix-run/node"' has no exported member 'Response'.

I skipped importing that Response (whatever it is) and tried with the base FetchAPI Response class, and got the following error:
Argument of type 'ReadWriteStream' is not assignable to parameter of type 'BodyInit | null | undefined'.

The full code of trying this one is the following

import { CacheProvider as EmotionCacheProvider } from '@emotion/react';
import { createReadableStreamFromReadable } from '@remix-run/node';
import { PassThrough } from 'node:stream';
import { RemixServer } from '@remix-run/react';
import { renderToPipeableStream } from 'react-dom/server';
import createEmotionCache from '@emotion/cache';
import createEmotionServer from '@emotion/server/create-instance';
import isbot from 'isbot';
import type { AppLoadContext, EntryContext } from '@remix-run/node';

const ABORT_DELAY = 5000;

export default function handleRequest(
    request: Request,
    responseStatusCode: number,
    responseHeaders: Headers,
    remixContext: EntryContext,
    // This is ignored so we can keep it in the template for visibility.  Feel
    // free to delete this parameter in your app if you're not using it!
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    loadContext: AppLoadContext
) {
    return isbot(request.headers.get('user-agent'))
        ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext)
        : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext);
}

function handleBotRequest(request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext) {
    return new Promise((resolve, reject) => {
        const emotionCache = createEmotionCache({ key: 'css' });
        let shellRendered = false;

        const { pipe, abort } = renderToPipeableStream(
            <EmotionCacheProvider value={emotionCache}>
                <RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />
            </EmotionCacheProvider>,
            {
                onAllReady() {
                    shellRendered = true;

                    const body = new PassThrough();
                    const emotionServer = createEmotionServer(emotionCache);
                    // const stream = createReadableStreamFromReadable(body);

                    const bodyWithStyles = emotionServer.renderStylesToNodeStream();
                    body.pipe(bodyWithStyles);

                    responseHeaders.set('Content-Type', 'text/html');

                    resolve(
                        new Response(bodyWithStyles, {
                            headers: responseHeaders,
                            status: responseStatusCode,
                        })
                    );

                    pipe(body);
                },
                onShellError(error: unknown) {
                    reject(error);
                },
                onError(error: unknown) {
                    responseStatusCode = 500;
                    // Log streaming rendering errors from inside the shell.  Don't log
                    // errors encountered during initial shell rendering since they'll
                    // reject and get logged in handleDocumentRequest.
                    if (shellRendered) {
                        console.error(error);
                    }
                },
            }
        );

        setTimeout(abort, ABORT_DELAY);
    });
}

function handleBrowserRequest(request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext) {
    return new Promise((resolve, reject) => {
        let shellRendered = false;
        const { pipe, abort } = renderToPipeableStream(<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />, {
            onShellReady() {
                shellRendered = true;
                const body = new PassThrough();
                const stream = createReadableStreamFromReadable(body);

                responseHeaders.set('Content-Type', 'text/html');

                resolve(
                    new Response(stream, {
                        headers: responseHeaders,
                        status: responseStatusCode,
                    })
                );

                pipe(body);
            },
            onShellError(error: unknown) {
                reject(error);
            },
            onError(error: unknown) {
                responseStatusCode = 500;
                // Log streaming rendering errors from inside the shell.  Don't log
                // errors encountered during initial shell rendering since they'll
                // reject and get logged in handleDocumentRequest.
                if (shellRendered) {
                    console.error(error);
                }
            },
        });

        setTimeout(abort, ABORT_DELAY);
    });
}

@Brocktho
Copy link

Brocktho commented Dec 27, 2023

@mbarni99

...I got the following error: Module '"@remix-run/node"' has no exported member 'Response'.

I skipped importing that Response (whatever it is) and tried with the base FetchAPI Response class, and got the following error: >Argument of type 'ReadWriteStream' is not assignable to parameter of type 'BodyInit | null | undefined'.

Remix used to ship a specific Response type that allowed you to dump in a ReadWriteStream into the Response, instead of maintaining that full Response object they now maintain a createReadableStreamFromReadable You would do mostly the same as what you had posted, but before slapping it all into the body of the Response you need to put it into the createReadableStreamFromReadable function like:

   // Import
   import { createReadableStreamFromReadable } from "@remix-run/node";
    // Other Code
   //
  //
           resolve(
                    new Response(createReadableStreamFromReadable(stream), {
                        headers: responseHeaders,
                        status: responseStatusCode,
                    })
                );

@michael-land
Copy link
Contributor

Is it possible to use material ui with remix spa mode?

@mahmoudmoravej
Copy link

The current Remix example is a bit outdated and hard to integrate with new Remix templates. I created a brand new integration (with Remix 2.8 + Vite). It helps alot in quick and smooth MUI + Remix integration and also addresses some challenges mentioned here.

Headsup! I am not great with Emotions. But just cross checking with current Remix + MU integration example, it seems it is working as expected with no need to use complex cache/style management.

cc @oliviertassinari @mnajdova

@khera
Copy link

khera commented May 1, 2024

The current Remix example is a bit outdated and hard to integrate with new Remix templates. I created a brand new integration (with Remix 2.8 + Vite). It helps alot in quick and smooth MUI + Remix integration and also addresses some challenges mentioned here.

Thank you so much for this. I was pounding my head for the last 4 hours until I just found this. Incorporating your MuiProvider.tsx file into my entry files was the trick I needed to incorporate MUI into my Remix app. The current "official" example is impossible to decipher as a newbie to Remix working on the current default template.

@mnajdova
Copy link
Member

mnajdova commented May 1, 2024

The current Remix example is a bit outdated and hard to integrate with new Remix templates. I created mahmoudmoravej/remix-mui#1. It helps alot in quick and smooth MUI + Remix integration and also addresses some challenges mentioned here.

@mahmoudmoravej would you like to update the current example we have and open a PR, we can then check what changes you've made.

@mahmoudmoravej
Copy link

mahmoudmoravej commented May 2, 2024

@mnajdova I did it but I am not sure it will be so helpful, becasue:

  • It is upgraded to Remix/Vite template, so you will see a bunch of unnecssary changes.
  • The approach I used is completely different, so there will not be a good side-by-side comparison.

I still think the PR I provided above(which simply added MUI on top of the latest version of Remix/Vite template) shows a better understanding of what and how it has been doe.

@cuzzlor
Copy link

cuzzlor commented May 28, 2024

The official example uses serverModuleFormat: 'cjs' in the remix config which seems to prevent fast hot-reloading of front-end changes.

I used the starter from @mnajdova: https://github.com/mahmoudmoravej/remix-mui and it seems to work very well so far. Much simpler, streaming (defer/suspense/await) seems to work fine, hot-reloading etc all seems to work perfectly.

@incarnateTheGreat
Copy link

The current Remix example is a bit outdated and hard to integrate with new Remix templates. I created a brand new integration (with Remix 2.8 + Vite). It helps alot in quick and smooth MUI + Remix integration and also addresses some challenges mentioned here.

Headsup! I am not great with Emotions. But just cross checking with current Remix + MU integration example, it seems it is working as expected with no need to use complex cache/style management.

cc @oliviertassinari @mnajdova

Thank you so much for this! It really helped!

@lookitskris
Copy link

The current Remix example is a bit outdated and hard to integrate with new Remix templates. I created a brand new integration (with Remix 2.8 + Vite). It helps alot in quick and smooth MUI + Remix integration and also addresses some challenges mentioned here.

Headsup! I am not great with Emotions. But just cross checking with current Remix + MU integration example, it seems it is working as expected with no need to use complex cache/style management.

cc @oliviertassinari @mnajdova

my guy - if I could tip you I would. Spent a full day going round and round in circles and this example just works!

@thedivac
Copy link

I have an issue with this template. If hydration fails and react falls back to client rendering the entire root, then the styles of emotion completely fail to load.
You can test this yourself easily if you just add a {Math.random()} to the root.tsx

@khera
Copy link

khera commented Nov 12, 2024

I have an issue with this template. If hydration fails and react falls back to client rendering the entire root, then the styles of emotion completely fail to load.

I observe this as well, but did not know what was the cause. I'd be interested in how to fix this flaw.

@cwagner22
Copy link

Did you find a solution for the styles not loading when hydration fails?

@thedivac
Copy link

Did you find a solution for the styles not loading when hydration fails?

No, I already had a working version (https://github.com/mui/material-ui/tree/master/examples/material-ui-remix-ts) that I wanted to replace with this cleaner setup.

@cwagner22
Copy link

cwagner22 commented Dec 1, 2024

I managed to find a way to make it work even when hydration fails:

MuiProvider.tsx

import { ThemeProvider } from '@mui/material';
import { theme } from './theme';
import { CacheProvider } from '@emotion/react';
import ClientStyleContext from 'ClientStyleContext';
import { useMemo, useState } from 'react';
import createCache from '@emotion/cache';

function createEmotionCache() {
  return createCache({ key: 'css' });
}

export function MuiProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const [cache, setCache] = useState(createEmotionCache());

  const clientStyleContextValue = useMemo(
    () => ({
      reset() {
        setCache(createEmotionCache());
      },
    }),
    [],
  );

  return (
    <ClientStyleContext.Provider value={clientStyleContextValue}>
      <CacheProvider value={cache}>
        <ThemeProvider theme={theme}>{children}</ThemeProvider>
      </CacheProvider>
    </ClientStyleContext.Provider>
  );
}

entry.server.tsx

const body = await renderToReadableStream(
  <MuiProvider>
      <ServerRouter
        context={routerContext}
        url={request.url}
        abortDelay={ABORT_DELAY}
      />
  </MuiProvider>
)

entry.client.tsx

hydrateRoot(
    document,
    <StrictMode>
        <MuiProvider>
            <HydratedRouter />  // (for React Router 7, or RemixBrowser for Remix)
        </MuiProvider>
    </StrictMode>
  );

ClientStyleContext.tsx

import * as React from 'react';

export interface ClientStyleContextData {
  reset: () => void;
}

export default React.createContext<ClientStyleContextData>({
  reset: () => {},
});

Layout of root.tsx

import { withEmotionCache } from '@emotion/react';
import ClientStyleContext from './ClientStyleContext';
import { unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/material';

export const Layout = withEmotionCache(
  ({ children }: { children: React.ReactNode }, emotionCache) => {
    const clientStyleData = useContext(ClientStyleContext);

    // Only executed on client
    useEnhancedEffect(() => {
      // re-link sheet container
      emotionCache.sheet.container = document.head;
      // re-inject tags
      const tags = emotionCache.sheet.tags;
      emotionCache.sheet.flush();
      tags.forEach((tag) => {
        (emotionCache.sheet as any)._insertTag(tag);
      });
      // reset cache to reapply global styles
      clientStyleData.reset();
    }, []);

     return (
      <html lang='en'>
        <head>
        ...

@khera
Copy link

khera commented Dec 5, 2024

@cwagner22 where does <HydratedRouter /> come from?

Also, what specifically of this whole configuration fixed the problem with hydration? It is similar to the material-ui-remix-ts example, so knowing why it was breaking would be helpful. Thanks!

@cwagner22
Copy link

HydratedRouter is for React Router 7, which was RemixBrowser in Remix.
I tried to find the part from material-ui-remix-ts that made it working and it seems it was the part from useEnhancedEffect, which I hope can be improved at some point.

@khera
Copy link

khera commented Dec 5, 2024

HydratedRouter is for React Router 7, which was RemixBrowser in Remix. I tried to find the part from material-ui-remix-ts that made it working and it seems it was the part from useEnhancedEffect, which I hope can be improved at some point.

Thanks for the clarification. I agree the useEnhancedEffect is likely the fix because it is re-applying the styles directly into the document.

I've implemented it and in dev. So far no errors and it properly refreshes even after I force an error in my code. Pushing out to staging to see if anyone can break it there.

@observerkei
Copy link

I integrated the content solution provided in this issue into Remix v2.51.1 + SPA mode which supports Vite build (https://github.com/observerkei/remix-spa-material-ui/tree/main) , 🎉 hope it helps.

Is it possible to use material ui with remix spa mode?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dx Related to developers' experience umbrella For grouping multiple issues to provide a holistic view
Projects
None yet
Development

No branches or pull requests