You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Next.js中实现类似Leetcode的在线代码编译器并解决localStorage报错

Next.js 接入在线编译器localStorage未定义问题解决方案

问题根因

Next.js默认会在服务端执行组件初始化逻辑,localStorage是浏览器端独有的全局API,服务端Node.js环境不存在该对象,你在组件constructor中直接读取localStorage的操作会在服务端渲染阶段触发localStorage is not defined错误。

可行解决方案

方案1:调整生命周期读取localStorage(通用方案,兼容所有Next.js版本)

  1. 移除constructor中直接读取localStorage的逻辑,给state设置初始默认值
  2. 在componentDidMount生命周期(仅会在客户端执行)中读取localStorage更新state
  3. 原有的写localStorage逻辑无需修改,所有事件回调都只会在客户端触发,不会出现异常

修改后的完整组件代码如下:

// Next.js 13+ App Router请在第一行加 'use client' 标记为客户端组件
'use client'
import React, { Component } from "react";
import "./Compiler.css";

export default class Compiler extends Component {
  constructor(props) {
    super(props);
    this.state = {
      input: '',
      output: '',
      language_id: 2,
      user_input: '',
      isClient: false
    };
  }

  componentDidMount() {
    // 仅客户端执行,读取localStorage更新状态
    this.setState({
      isClient: true,
      input: localStorage.getItem('input') || '',
      language_id: localStorage.getItem('language_Id') || 2
    })
  }

  input = (event) => {
    event.preventDefault();
    this.setState({ input: event.target.value });
    localStorage.setItem('input', event.target.value)
  };

  userInput = (event) => {
    event.preventDefault();
    this.setState({ user_input: event.target.value });
  };

  language = (event) => {
    event.preventDefault();
    this.setState({ language_id: event.target.value });
    localStorage.setItem('language_Id',event.target.value)
  };

  submit = async (e) => {
    e.preventDefault();
    let outputText = document.getElementById("output");
    outputText.innerHTML = "";
    outputText.innerHTML += "Creating Submission ...\n";
    const response = await fetch(
      "https://judge0-ce.p.rapidapi.com/submissions",
      {
        method: "POST",
        headers: {
          "x-rapidapi-host": "judge0-ce.p.rapidapi.com",
          "x-rapidapi-key": "*****************",
          "content-type": "application/json",
          accept: "application/json",
        },
        body: JSON.stringify({
          source_code: this.state.input,
          stdin: this.state.user_input,
          language_id: this.state.language_id,
        }),
      }
    );
    outputText.innerHTML += "Submission Created ...\n";
    const jsonResponse = await response.json();

    let jsonGetSolution = {
      status: { description: "Queue" },
      stderr: null,
      compile_output: null,
    };

    while (
      jsonGetSolution.status.description !== "Accepted" &&
      jsonGetSolution.stderr == null &&
      jsonGetSolution.compile_output == null
    ) {
      outputText.innerHTML = `Creating Submission ... \nSubmission Created ...\nChecking Submission Status\nstatus : ${jsonGetSolution.status.description}`;
      if (jsonResponse.token) {
        let url = `https://judge0-ce.p.rapidapi.com/submissions/${jsonResponse.token}?base64_encoded=true`;
        const getSolution = await fetch(url, {
          method: "GET",
          headers: {
            "x-rapidapi-host": "judge0-ce.p.rapidapi.com",
            "x-rapidapi-key": "******************",
            "content-type": "application/json",
          },
        });
        jsonGetSolution = await getSolution.json();
      }
    }
    if (jsonGetSolution.stdout) {
      const output = atob(jsonGetSolution.stdout);
      outputText.innerHTML = "";
      outputText.innerHTML += `Results :\n${output}\nExecution Time : ${jsonGetSolution.time} Secs\nMemory used : ${jsonGetSolution.memory} bytes`;
    } else if (jsonGetSolution.stderr) {
      const error = atob(jsonGetSolution.stderr);
      outputText.innerHTML = "";
      outputText.innerHTML += `\n Error :${error}`;
    } else {
      const compilation_error = atob(jsonGetSolution.compile_output);
      outputText.innerHTML = "";
      outputText.innerHTML += `\n Error :${compilation_error}`;
    }
  };

  render() {
    // 避免服务端和客户端渲染内容不一致,客户端初始化完成后再渲染组件内容
    if (!this.state.isClient) return null;
    return (
      <>
        <div className="row container-fluid">
          <div className="col-6 ml-4 ">
            <label htmlFor="solution ">
              <span className="badge badge-info heading mt-2 ">
                <i className="fas fa-code fa-fw fa-lg"></i> Code Here
              </span>
            </label>
            <textarea
              required
              name="solution"
              id="source"
              onChange={this.input}
              className=" source"
              value={this.state.input}
            ></textarea>

            <button
              type="submit"
              className="btn btn-danger ml-2 mr-2 "
              onClick={this.submit}
            >
              <i className="fas fa-cog fa-fw"></i> Run
            </button>

            <label htmlFor="tags" className="mr-1">
              <b className="heading">Language:</b>
            </label>
            <select
              value={this.state.language_id}
              onChange={this.language}
              id="tags"
              className="form-control form-inline mb-2 language"
            >
              <option value="54">C++</option>
              <option value="50">C</option>
              <option value="62">Java</option>
              <option value="71">Python</option>
            </select>
          </div>
          <div className="col-5">
            <div>
              <span className="badge badge-info heading my-2 ">
                <i className="fas fa-exclamation fa-fw fa-md"></i> Output
              </span>
              <textarea id="output"></textarea>
            </div>
          </div>
        </div>

        <div className="mt-2 ml-5">
          <span className="badge badge-primary heading my-2 ">
            <i className="fas fa-user fa-fw fa-md"></i> User Input
          </span>
          <br />
          <textarea id="input" onChange={this.userInput}></textarea>
        </div>
      </>
    );
  }
}

方案2:禁用组件SSR(适合Pages Router)

如果不想修改原组件代码,可以在导入Compiler组件时使用Next.js动态导入能力关闭SSR,组件只会在客户端加载:

import dynamic from 'next/dynamic'
const Compiler = dynamic(() => import('../components/Compiler'), { ssr: false })

额外优化建议

  • 代码中直接操作DOM的逻辑可以改成用React state管理output内容,更符合React设计规范,避免DOM操作异常
  • Judge0的API密钥不要硬编码在前端代码中,容易泄露被盗刷,建议封装到自己的后端接口代理请求

内容的提问来源于stack exchange,提问作者Farhan Ahmed

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 01:45:05