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

redirect_to不生效:Stimulus请求后UI未自动更新问题

问题

我在Rails后端写了publish和unpublish控制器方法,执行后调用redirect_to跳转到编辑页;前端用Stimulus控制器通过fetch发送PUT请求。路由和页面调用都正常,数据库状态会更新,但点击“发布”或“取消发布”后UI不会自动刷新,必须手动刷新页面才能看到状态变化。原本以为redirect_to能实现UI自动更新,但没生效,想解决无需手动刷新同步UI状态的问题。

原代码

后端控制器

def publish
  post = Post.find(params[:post_id])
  post.update(published: true, published_at: Time.now)
  redirect_to edit_post_path(post)
end

def unpublish
  post = Post.find(params[:post_id])
  post.update(published: false, published_at: nil)
  redirect_to edit_post_path(post)
end

Stimulus控制器

import { Controller } from "@hotwired/stimulus"
import log from "stimulus_reflex/javascript/log";

// Connects to data-controller="publishers"
export default class extends Controller {
  connect() {
    console.log("Connected")
  }

  publish(event){
    event.preventDefault();
    const postId = event.currentTarget.getAttribute("data-post-id");
    const url = `/posts/${postId}/publish`; 

    fetch(url, {
      method: "PUT",
      headers: {
        "Accept": "application/json",
        "Content-Type": "application/json",
        "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute('content')
      },
    })
      
  }

  unpublish(event){
    event.preventDefault();
    const postId = event.currentTarget.getAttribute('data-post-id');
    const url = `/posts/${postId}/unpublish`;
    fetch(url, {
      method: "PUT",
      headers: {
        "Accept": "application/json",
        "Content-Type": "application/json",
        "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute('content')
      },
    })
    
    }
}

路由配置(route.rb)

scope module: "authors" do
    resources :posts do
      put "publish", to: "posts#publish"
      put "unpublish", to: "posts#unpublish"
      resources :elements do
      end
    end
  end

页面模板(edit.html.erb)

<div class="card-footer">
        <% if @post.published? %>
          <a
            href="#"
            class="btn btn-danger d-block my-4"
            data-action="click->publishers#unpublish"
            data-post-id="<%= @post.id %>"
          >Unpublish</a>
        <% else %>
          <a
            href="#"
            class="btn btn-success d-block my-4"
            data-action="click->publishers#publish"
            data-post-id="<%= @post.id %>"
          >Publish</a>
        <% end %>
      </div>

问题原因

用fetch发送AJAX请求时,后端的redirect_to只会返回302重定向响应,但AJAX请求不会自动触发页面跳转——浏览器只有在处理普通HTTP请求时才会执行重定向。所以前端收到响应后不会刷新页面,UI自然不会同步更新。

解决方案

方案一:后端返回JSON,前端手动更新DOM

这种方式不需要引入额外依赖,直接修改现有代码即可。

  1. 修改后端控制器:
    把redirect_to改成返回JSON数据,告诉前端操作结果和当前发布状态:
def publish
  post = Post.find(params[:post_id])
  post.update(published: true, published_at: Time.now)
  render json: { success: true, published: post.published? }
end

def unpublish
  post = Post.find(params[:post_id])
  post.update(published: false, published_at: nil)
  render json: { success: true, published: post.published? }
end
  1. 修改Stimulus控制器:
    在fetch请求完成后,根据后端返回的状态替换按钮元素:
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    console.log("Connected")
  }

  publish(event){
    event.preventDefault();
    const postId = event.currentTarget.getAttribute("data-post-id");
    const url = `/posts/${postId}/publish`; 

    fetch(url, {
      method: "PUT",
      headers: {
        "Accept": "application/json",
        "Content-Type": "application/json",
        "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute('content')
      },
    })
    .then(response => response.json())
    .then(data => {
      if (data.success) {
        // 替换成取消发布按钮
        const cardFooter = event.currentTarget.closest('.card-footer');
        cardFooter.innerHTML = `
          <a
            href="#"
            class="btn btn-danger d-block my-4"
            data-action="click->publishers#unpublish"
            data-post-id="${postId}"
          >Unpublish</a>
        `;
      }
    })
    .catch(error => console.error('Error:', error));
  }

  unpublish(event){
    event.preventDefault();
    const postId = event.currentTarget.getAttribute('data-post-id');
    const url = `/posts/${postId}/unpublish`;
    fetch(url, {
      method: "PUT",
      headers: {
        "Accept": "application/json",
        "Content-Type": "application/json",
        "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute('content')
      },
    })
    .then(response => response.json())
    .then(data => {
      if (data.success) {
        // 替换成发布按钮
        const cardFooter = event.currentTarget.closest('.card-footer');
        cardFooter.innerHTML = `
          <a
            href="#"
            class="btn btn-success d-block my-4"
            data-action="click->publishers#publish"
            data-post-id="${postId}"
          >Publish</a>
        `;
      }
    })
    .catch(error => console.error('Error:', error));
  }
}

方案二:用Turbo Stream实现自动DOM更新(推荐,符合Rails Hotwire生态)

如果项目已经引入Turbo,这种方式更简洁,不需要手动拼接HTML。

  1. 修改后端控制器:
    返回Turbo Stream指令,告诉前端替换指定DOM元素:
def publish
  post = Post.find(params[:post_id])
  post.update(published: true, published_at: Time.now)
  respond_to do |format|
    format.turbo_stream do
      render turbo_stream: turbo_stream.replace(
        "publish-button-container",
        partial: "authors/posts/publish_button",
        locals: { post: post }
      )
    end
  end
end

def unpublish
  post = Post.find(params[:post_id])
  post.update(published: false, published_at: nil)
  respond_to do |format|
    format.turbo_stream do
      render turbo_stream: turbo_stream.replace(
        "publish-button-container",
        partial: "authors/posts/publish_button",
        locals: { post: post }
      )
    end
  end
end
  1. 创建按钮部分模板:
    新建app/views/authors/posts/_publish_button.html.erb,把按钮逻辑抽出来:
<a
  href="#"
  class="btn <%= post.published? ? 'btn-danger' : 'btn-success' %> d-block my-4"
  data-action="click->publishers#<%= post.published? ? 'unpublish' : 'publish' %>"
  data-post-id="<%= post.id %>"
>
  <%= post.published? ? 'Unpublish' : 'Publish' %>
</a>
  1. 修改edit.html.erb模板:
    给按钮容器加上唯一ID,方便Turbo定位替换:
<div class="card-footer" id="publish-button-container">
  <%= render partial: "authors/posts/publish_button", locals: { post: @post } %>
</div>
  1. 简化Stimulus控制器:
    去掉JSON相关的请求头,Turbo会自动处理Turbo Stream响应:
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    console.log("Connected")
  }

  publish(event){
    event.preventDefault();
    const postId = event.currentTarget.getAttribute("data-post-id");
    const url = `/posts/${postId}/publish`; 

    fetch(url, {
      method: "PUT",
      headers: {
        "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute('content')
      },
    })
    .catch(error => console.error('Error:', error));
  }

  unpublish(event){
    event.preventDefault();
    const postId = event.currentTarget.getAttribute('data-post-id');
    const url = `/posts/${postId}/unpublish`;
    fetch(url, {
      method: "PUT",
      headers: {
        "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute('content')
      },
    })
    .catch(error => console.error('Error:', error));
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 00:30:58