我正在学习 Firebase、构建 React 应用程序(React 18.2.0)的课程。 为了防止应用程序在设置状态时崩溃/冻结,我们将该状态设置包装到一个
startTransition()
函数中。
我们可以直接从 React 导入这个函数或者从 hook 中获取它:
const [isPending, startTransition] = useTransition();
到目前为止,我认为这两种方法之间的唯一区别(也根据文档)是: A.我们只能在顶层功能组件和自定义钩子上使用钩子 B. 使用钩子为我们提供了方便的
isPending
布尔值,指示过渡是否仍在进行,因此可以使用它在 UI 中反映它。
但是,如果我直接导入
startTransition
,它可以工作,但是用完全相同的代码从钩子中取出它,而不使用 isPending
,它不会,它会冻结并崩溃。
我尽力寻找答案。我想了解我做错了什么。可能是因为我用了TypeScript,但是课程没有,需要打字?
这是 App.tsx 的代码,我在其中注释掉了一种方法或另一种方法。抱歉,有好几行,
startTransition()
在第99行,我们在这里声明:
const handleEditRecipeClick = (recipeId: string) => {
startTransition(() => {
const selectedRecipe = recipes.find((recipe) => recipeId === recipe.id);
if (selectedRecipe) {
setCurrentRecipe(selectedRecipe);
console.log(selectedRecipe);
}
});
window.scrollTo(0, document.body.scrollHeight);
};
任何线索将不胜感激:)
import {
useCallback,
useEffect,
useState,
useTransition,
// startTransition,
} from "react";
import firebase from "firebase/compat";
import "./App.css";
import FirebaseAuthService from "./FirebaseAuthService";
import FirebaseFirestoreService, {
queryType,
recipeType,
recipeWithIdType,
} from "./FirebaseFirestoreService";
import LoginForm from "./components/LoginForm";
import AddEditRecipeForm from "./components/AddEditRecipeForm";
function App() {
const [isPending, startTransition] = useTransition();
const [user, setUser] = useState<firebase.User | null>(null);
const [currentRecipe, setCurrentRecipe] = useState<recipeWithIdType | null>(
null
);
const [recipes, setRecipes] = useState<recipeWithIdType[]>([]);
const fetchRecipes = useCallback(async () => {
const queries: queryType[] = [];
if (!user) {
queries.push({
field: "isPublished",
condition: "==",
value: true,
});
}
try {
const response = await FirebaseFirestoreService.readDocuments(
"recipes",
queries
);
const newRecipes = response.docs.map((recipeDoc) => {
const id = recipeDoc.id;
const data = recipeDoc.data() as recipeType;
return { ...data, id };
});
setRecipes(newRecipes);
} catch (error) {
if (error instanceof Error) {
alert(error.message);
}
}
}, [user]);
useEffect(() => {
fetchRecipes();
}, [fetchRecipes, user]);
FirebaseAuthService.subcribeToAuthCahnges(setUser);
const handleAddRecipe = async (newRecipe: recipeType) => {
try {
const response = await FirebaseFirestoreService.createDocument(
"recipes",
newRecipe
);
fetchRecipes();
alert(`New recipe created with ID: ${response.id}`);
} catch (error) {
if (error instanceof Error) {
alert(error.message);
}
}
};
const handleUpdateRecipe = async (
updatedRecipe: recipeType,
recipeId: string
) => {
try {
await FirebaseFirestoreService.updateDocument(
"recipes",
recipeId,
updatedRecipe
);
fetchRecipes();
alert(`New recipe created with ID: ${recipeId}`);
setCurrentRecipe(null);
} catch (error) {
if (error instanceof Error) {
alert(error.message);
}
}
};
const handleEditRecipeClick = (recipeId: string) => {
startTransition(() => {
const selectedRecipe = recipes.find((recipe) => recipeId === recipe.id);
if (selectedRecipe) {
setCurrentRecipe(selectedRecipe);
console.log(selectedRecipe);
}
});
window.scrollTo(0, document.body.scrollHeight);
};
const handleEditRecipeCancel = () => {
setCurrentRecipe(null);
};
//can be more detailed here about types and constatnts, but this is simple
//more info in button types in crwn-clothing project
const CATEGORIES = {
breadsSandwichesAndPizza: "Breads, Sandwiches and Pizza",
eggsAndBreakfast: "Eggs & Breakfast",
dessertsAndBakedGoods: "Desserts & Baked Goods",
fishAndSeafood: "Fish & Seafood",
vegetables: "Vegetables",
};
const lookupCategoryLabel = (categoryKey: string) =>
CATEGORIES[categoryKey as keyof typeof CATEGORIES];
//formating the date
const formatDate = (timestamp: number) => {
const date = new Date(timestamp);
const day = date.getUTCDate();
const month = date.toLocaleString("default", { month: "long" });
const year = date.getFullYear();
return `${day} of ${month}, ${year}`;
};
return (
<div className="App">
<div className="title-row">
<h1 className="title">Firebase Recipes</h1>
<LoginForm existingUser={user} />
</div>
<div className="main">
<div className="center">
<div className="recipe-list-box">
{recipes && recipes.length > 0 && (
<div className="recipe-list">
{recipes.map((recipe) => (
<div className="recipe-card" key={recipe.id}>
{!recipe.isPublished && (
<div className="unpublished">UNPUBLISHED</div>
)}
<div className="recipe-name">{recipe.name}</div>
<div className="recipe-field">
Category: {lookupCategoryLabel(recipe.category)}
</div>
<div className="recipe-field">
Published: {formatDate(recipe.publishDate)}
</div>
{user && (
<button
type="button"
className="primary-button edit-button"
onClick={() => handleEditRecipeClick(recipe.id)}
>
EDIT
</button>
)}
</div>
))}
</div>
)}
</div>
</div>
{/* {isPending && <p>Loading...</p>} */}
{user && (
<AddEditRecipeForm
// existingRecipe={currentRecipe}
onAddRecipe={handleAddRecipe}
// onUpdateRecipe={handleUpdateRecipe}
// onCancel={handleEditRecipeCancel}
/>
)}
</div>
</div>
);
}
export default App;
如果我注释掉 useTransition 钩子,并直接从 React 导入函数,那么一切都有效。据我了解,它应该是相同的功能!我尝试用钩子实现方法,玩额外的功能并更好地理解它,但即使我只使用
startTransition()
而不更改任何代码,它也不起作用
useTransition
导致 2 次重新渲染。
https://github.com/facebook/react/issues/24269
我面临着同样的问题,就我而言,问题是我没有考虑将
isPending
切换到 true
的第一次“紧急”重新渲染。
这里有一篇好文章详细解释了这一点