Next.js Discord

Discord Forum

How to run code on the client-side only?

Unanswered
Gharial posted this in #help-forum
Open in Discord
GharialOP
Hi, pretty new to Next.js and still trying wrap my head around some concepts like where things run so this is probably a basic question. What I'm trying to do is create a simple theme switcher that uses window.getComputedStyle on the browser to update a few CSS variables.

Initially I had a component labeled with 'use client' and a utility function that did this change. The function works fine, the real problem is on the initial page load as it seems to run both on the client and the server, causing an error (ReferenceError: getComputedStyle is not defined). Browsing online I read that I should use a hook for client-side only code, so I wrote a basic useTheme hook and moved all the logic there, but still the same error.

The only solution that I found to this to include a check to determine the environment. This works but seems a bit clunky, even though it's just this one line. So, is there a better way to ensure that certain code only runs on the client? Thank you, and this is the function in question btw in case it makes any difference:

export function getVariableValue(color: AccentOptions, theme: ThemeOptions) {
  if (typeof window === "undefined") return ""; // do not run on server side
  return getComputedStyle(document.documentElement).getPropertyValue(
    `--ctp-${variant}-${color}`,
  );
}

14 Replies

@Gharial Hi, pretty new to Next.js and still trying wrap my head around some concepts like where things run so this is probably a basic question. What I'm trying to do is create a simple theme switcher that uses `window.getComputedStyle` on the browser to update a few CSS variables. Initially I had a component labeled with 'use client' and a utility function that did this change. The function works fine, the real problem is on the initial page load as it seems to run both on the client and the server, causing an error (ReferenceError: getComputedStyle is not defined). Browsing online I read that I should use a hook for client-side only code, so I wrote a basic `useTheme` hook and moved all the logic there, but still the same error. The only solution that I found to this to include a check to determine the environment. This works but seems a bit clunky, even though it's just this one line. So, is there a better way to ensure that certain code only runs on the client? Thank you, and this is the function in question btw in case it makes any difference: ts export function getVariableValue(color: AccentOptions, theme: ThemeOptions) { if (typeof window === "undefined") return ""; // do not run on server side return getComputedStyle(document.documentElement).getPropertyValue( `--ctp-${variant}-${color}`, ); }
"use client" does not change the rendering method to CSR, it just means the code will run both on the server and on the client. there are multiple ways to run code only on the client but it depends a lot on what you are trying to do. the snippet you sent is not enough to give you a proper answer because it depends on where you are calling it, it could or could not cause a hydration mismatch
GharialOP
Ahh, I see I thought it would only run on the client. Thanks for clarifying that, let me get the code online real quick.
GharialOP
So this is a minimal version to reproduce the issue. For convenience, is all in one file (let me know please, if that makes any difference) but in reality is split into two: the component itself and a utility file with some functions; the one causing trouble is getVariableValue which updates the value of a CSS variable using window.getComputedStyle.

Notice the span element with an inline style, it uses the returned value from this function which is where the issue stems from. The goal, is to have a series of rounded buttons each with its own color, like in the screenshot:

"use client";

import { useState } from 'react';

function getVariableValue(color, theme) {
  const variant = theme.split(" ").at(-1)?.toLowerCase();
  return getComputedStyle(document.documentElement).getPropertyValue(
    `--ctp-${variant}-${color}`,
);

export default function ThemeController() {
  const [currentTheme, setCurrentTheme] = useState('mocha');
  const [currentAccent, setCurrentAccent] = useState('green');
  return (
    <div>
      {/* omitted */}
      <ul>
        <p>Accent</p>
        {['green', 'red', 'yellow'].map((accentColor, idx) => (
          <li key={idx}>
            <button>
              <span
                className="circle"
                style={{
                  backgroundColor: getVariableValue(accentColor, currentTheme),
                }}
              />
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
I'm sure there are better ways to do this, and I'm open to suggestions, but for now this is just a prototype to get my feet wet with Next.js
GharialOP
Does it have to be useEffect then? I understood that any hook would do.
@Gharial So this is a minimal version to reproduce the issue. For convenience, is all in one file (let me know please, if that makes any difference) but in reality is split into two: the component itself and a utility file with some functions; the one causing trouble is `getVariableValue` which updates the value of a CSS variable using `window.getComputedStyle`. Notice the span element with an inline style, it uses the returned value from this function which is where the issue stems from. The goal, is to have a series of rounded buttons each with its own color, like in the screenshot: js "use client"; import { useState } from 'react'; function getVariableValue(color, theme) { const variant = theme.split(" ").at(-1)?.toLowerCase(); return getComputedStyle(document.documentElement).getPropertyValue( `--ctp-${variant}-${color}`, ); export default function ThemeController() { const [currentTheme, setCurrentTheme] = useState('mocha'); const [currentAccent, setCurrentAccent] = useState('green'); return ( <div> {/* omitted */} <ul> <p>Accent</p> {['green', 'red', 'yellow'].map((accentColor, idx) => ( <li key={idx}> <button> <span className="circle" style={{ backgroundColor: getVariableValue(accentColor, currentTheme), }} /> </button> </li> ))} </ul> </div> ); }
the problem is that you are trying to render a function that only works on the client during the render which runs on the server. to correctly fix it you need to create a state with a default value that the server can use and swap it to the client value within useEffect (which only runs in the client, so calling the function will not give you an error)
keep in mind that if this component is rendered directly on the page, you will notice a flash of styles because the server always start with a default value
GharialOP
Mmm seems pretty obvious now when you say it like that 😂
Yeah, luckily this is hidden in a dropdown menu so it won't be an issue, I think, but I will keep that in mind about the flash
I don't understand why you are calling getComputedStyle for this tho. if you know the name of the CSS variable on the server why don't you just set the backgroundColor to the variable instead of trying to get its value manually?
GharialOP
I'm actually using useLocalStorage (from usehooks-ts) to get the initial value, so it may not always be the same for everyone. I guess for the initial load as the default, it makes sense. I just didn't think of that but you are probably right, that's the better solution for the default value
Yep, that actually works much better 😂
Well, I was trying to find a clever solution but sometimes less is more. Thanks a lot for the help guys, much appreciated.