final migration over to react_on_rails / react18

This commit is contained in:
Dylan Knutson
2023-04-04 23:10:15 +09:00
parent 18fff3bc07
commit f317aa273e
22 changed files with 107 additions and 125 deletions

View File

@@ -1,31 +0,0 @@
import PropTypes from "prop-types";
import React, { useState } from "react";
import style from "./HelloWorld.module.css";
const HelloWorld = (props) => {
const [name, setName] = useState(props.name);
return (
<div>
<h3>Hello, {name}!</h3>
<hr />
<form>
<label className={style.bright} htmlFor="name">
Say hello to:
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
</form>
</div>
);
};
HelloWorld.propTypes = {
name: PropTypes.string.isRequired, // this is passed from the Rails view
};
export default HelloWorld;

View File

@@ -1,4 +0,0 @@
.bright {
color: green;
font-weight: bold;
}

View File

@@ -1,5 +0,0 @@
import HelloWorld from './HelloWorld';
// This could be specialized for server rendering
// For example, if using React-Router, we'd have the SSR setup here.
export default HelloWorld;

View File

@@ -1,12 +1,12 @@
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { debounce, isEmpty } from "lodash";
import * as React from "react";
import ListItem from "./ListItem";
import { useCallback, useEffect, useRef, useState } from "react";
import Icon from "./Icon";
import ListItem from "./ListItem";
// staging
const HOST = "http://scraper.local:3001";
// const HOST = "";
// const HOST = "http://scraper.local:3001";
const HOST = "";
const COMMON_LIST_ELEM_CLASSES = `
w-full p-2 pl-8
text-xl font-light
@@ -26,17 +26,27 @@ const INPUT_ELEM_CLASSES = `
placeholder:font-extralight
`;
interface PropTypes {}
export default function UserSearchBar({}: PropTypes) {
interface PropTypes {
isServerRendered?: boolean;
}
interface ServerResponse {
users: { name: string }[];
}
export default function UserSearchBar({ isServerRendered }: PropTypes) {
isServerRendered = !!isServerRendered;
const [userName, setUserName] = useState("");
const [items, setUserList] = useState([]);
const [selectedIdx, setSelectedIdx] = useState(null);
const [showSpinner, setShowSpinner] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [pendingRequest, setPendingRequest] = useState(null);
const [items, setUserList] = useState<ServerResponse["users"]>([]);
const [selectedIdx, setSelectedIdx] = useState<number | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [pendingRequest, setPendingRequest] = useState<AbortController | null>(
null
);
const [typingSettled, setTypingSettled] = useState(true);
const inputRef = useRef(null);
const inputHasFocus = document.activeElement == inputRef.current;
const [isFocused, setIsFocused] = useState(
isServerRendered ? false : document.activeElement == inputRef.current
);
const clearResults = useCallback(() => {
setUserList([]);
@@ -81,22 +91,26 @@ export default function UserSearchBar({}: PropTypes) {
const controller = new AbortController();
setPendingRequest(controller);
setShowSpinner(true);
fetch(`${HOST}/api/fa/search_users?name=${encodeURIComponent(userName)}`, {
signal: controller.signal,
})
.then(handleServerResponse)
.catch((err) => {
if (err.code != err.ABORT_ERR) {
clearResults();
setErrorMessage(err.toString());
async function doFetch() {
try {
let response = await fetch(
`${HOST}/api/fa/search_users?name=${encodeURIComponent(userName)}`,
{
signal: controller.signal,
}
);
await handleServerResponse(response);
} catch (error) {
if (error.name == "AbortError") {
return;
}
})
.finally(() => {
setShowSpinner(false);
clearResults();
setErrorMessage(error.toString());
} finally {
setPendingRequest(null);
});
}
}
doFetch();
return () => {
cancelPendingRequest();
};
@@ -104,9 +118,9 @@ export default function UserSearchBar({}: PropTypes) {
const handleServerResponse = useCallback(async (response) => {
clearResults();
const contentType = response.headers.get("Content-Type");
const contentType: string = response.headers.get("Content-Type");
if (response.status == 200) {
const { users } = await response.json();
const { users }: ServerResponse = await response.json();
setUserList(users);
} else if (
!isEmpty(contentType) &&
@@ -136,6 +150,11 @@ export default function UserSearchBar({}: PropTypes) {
case "ArrowUp":
selectPrevListElem();
break;
case "Enter":
if (selectedIdx != null) {
setUserName(items[selectedIdx].name);
}
break;
default:
return;
}
@@ -164,18 +183,15 @@ export default function UserSearchBar({}: PropTypes) {
setSelectedIdx(newIdx);
}
const errorShown = isFocused && !isEmpty(errorMessage);
const infoShown =
inputHasFocus &&
isFocused &&
!isEmpty(userName) &&
pendingRequest == null &&
typingSettled &&
items.length == 0;
const errorShown = !isEmpty(errorMessage);
const itemsShown =
inputHasFocus &&
items.length > 0 &&
typingSettled &&
pendingRequest == null;
isFocused && items.length > 0 && typingSettled && pendingRequest == null;
const anyShown = infoShown || errorShown || itemsShown;
function UserSearchBarItems() {
@@ -184,7 +200,7 @@ export default function UserSearchBar({}: PropTypes) {
{errorShown ? (
<ListItem
key="error"
isLast={items.length == 0}
isLast={!infoShown && items.length == 0}
selected={false}
style="error"
value={errorMessage}
@@ -193,7 +209,7 @@ export default function UserSearchBar({}: PropTypes) {
{infoShown ? (
<ListItem
key="info"
isLast={true}
isLast={!itemsShown}
selected={false}
style="info"
value="No users found"
@@ -202,7 +218,7 @@ export default function UserSearchBar({}: PropTypes) {
{itemsShown
? items.map(({ name }, idx) => (
<ListItem
key={name}
key={"name-" + name}
isLast={idx == items.length - 1}
selected={idx == selectedIdx}
style="item"
@@ -227,7 +243,7 @@ export default function UserSearchBar({}: PropTypes) {
type="magnifying-glass"
className={`ml-2 ${FOCUSABLE_SVG_ELEM_CLASSES}`}
/>
{showSpinner ? (
{pendingRequest != null ? (
<Icon type="spinner" className={`right-2 ${SVG_ELEM_CLASSES}`} />
) : null}
<input
@@ -243,6 +259,8 @@ export default function UserSearchBar({}: PropTypes) {
defaultValue={userName}
onChange={(v) => searchForUser(v.target.value)}
onKeyDown={(v) => onSearchInputKeyDown(v)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
ref={inputRef}
/>
</label>

View File

@@ -0,0 +1,6 @@
import * as React from "react";
import UserSearchBar from "./UserSearchBar";
export default function (props) {
return <UserSearchBar {...props} isServerRendered={true} />;
}

View File

@@ -0,0 +1,7 @@
import ReactOnRails from "react-on-rails";
import UserSearchBar from "../bundles/Main/components/UserSearchBar";
// This is how react_on_rails can see the UserSearchBar in the browser.
ReactOnRails.register({
UserSearchBar,
});

View File

@@ -1,4 +0,0 @@
// Support component names relative to this directory:
var componentRequireContext = require.context("components", true);
var ReactRailsUJS = require("react_ujs");
ReactRailsUJS.useContext(componentRequireContext);

View File

@@ -1,8 +0,0 @@
import ReactOnRails from 'react-on-rails';
import HelloWorld from '../bundles/HelloWorld/components/HelloWorld';
// This is how react_on_rails can see the HelloWorld in the browser.
ReactOnRails.register({
HelloWorld,
});

View File

@@ -1,8 +1,8 @@
import ReactOnRails from 'react-on-rails';
import ReactOnRails from "react-on-rails";
import HelloWorld from '../bundles/HelloWorld/components/HelloWorldServer';
import UserSearchBar from "../bundles/Main/components/UserSearchBarServer";
// This is how react_on_rails can see the HelloWorld in the browser.
// This is how react_on_rails can see the UserSearchBar in the browser.
ReactOnRails.register({
HelloWorld,
UserSearchBar,
});

View File

@@ -1,5 +0,0 @@
// By default, this pack is loaded for server-side rendering.
// It must expose react_ujs as `ReactRailsUJS` and prepare a require context.
var componentRequireContext = require.context("components", true);
var ReactRailsUJS = require("react_ujs");
ReactRailsUJS.useContext(componentRequireContext);

View File

@@ -1,2 +0,0 @@
<h1>Hello World</h1>
<%= react_component("HelloWorld", props: @hello_world_props, prerender: false) %>

View File

@@ -3,6 +3,15 @@
<head>
<title><%= @site_title %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<% if Object.const_defined?('Rack::MiniProfiler') %>
<%# needed so miniprofiler doens't screw with the fetch() api %>
<script type='text/javascript'>
if(!window.MiniProfiler) {
window.MiniProfiler = {};
}
window.MiniProfiler.patchesApplied = true;
</script>
<% end %>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>ReactOnRailsWithShakapacker</title>
<%= csrf_meta_tags %>
<%= javascript_pack_tag 'hello-world-bundle' %>
<%= stylesheet_pack_tag 'hello-world-bundle' %>
</head>
<body>
<%= yield %>
</body>
</html>

View File

@@ -1,8 +1,6 @@
<% content_for :head do %>
<%#= javascript_pack_tag "application" %>
<%= javascript_pack_tag 'hello-world-bundle' %>
<%= javascript_pack_tag 'application-bundle' %>
<% end %>
<div class='w-full mt-2 sm:mt-6'>
<%#= react_component("UserSearchBar", {}, prerender: true) %>
<%= react_component("HelloWorld", props: {}, prerender: true) %>
<%= react_component("UserSearchBar", props: {}, prerender: true) %>
</div>

View File

@@ -1,5 +1,4 @@
Rails.application.routes.draw do
get 'hello_world', to: 'hello_world#index'
root to: "pages#root"
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

10
config/webpack/staging.js Normal file
View File

@@ -0,0 +1,10 @@
// The source code including full typescript support is available at:
// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/production.js
const webpackConfig = require('./webpackConfig');
const productionEnvOnly = (_clientWebpackConfig, _serverWebpackConfig) => {
// place any code here that is for production only
};
module.exports = webpackConfig(productionEnvOnly);

View File

@@ -37,13 +37,13 @@ development:
# port: 8080
compress: true
# Note that apps that do not check the host are vulnerable to DNS rebinding attacks
allowed_hosts: [ 'localhost' ]
allowed_hosts: ["localhost"]
pretty: true
headers:
'Access-Control-Allow-Origin': '*'
"Access-Control-Allow-Origin": "*"
static:
watch:
ignored: '**/node_modules/**'
ignored: "**/node_modules/**"
test:
<<: *default
@@ -52,11 +52,12 @@ test:
# Compile test packs to a separate directory
public_output_path: packs-test
production:
production: &production
<<: *default
# Production depends on precompilation of packs prior to booting for performance.
compile: false
# Cache manifest.json for performance
cache_manifest: true
staging:
<<: *production

View File

@@ -44,6 +44,7 @@
],
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@types/lodash": "^4.14.192",
"@types/react": "^18.0.33",
"react-refresh": "^0.14.0"
}

View File

@@ -1218,6 +1218,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
"@types/lodash@^4.14.192":
version "4.14.192"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.192.tgz#5790406361a2852d332d41635d927f1600811285"
integrity sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==
"@types/mime@*":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"