reactjsreact-state

Why Isn't i18n Translation Updating with useState in React?


I have an input with a label. The label has some translations as well as some variations that happen depending on which page the user is so I put translation keys inside useState to watch changes and react to them by changing the label text. The problem is when I pass the translation key to useState and change language, the text inside useState doesn't translate.

I marked the useState in question with **.

import CloseIcon from "@mui/icons-material/Close";
import SearchIcon from "@mui/icons-material/Search";
import { Box, IconButton, TextField } from "@mui/material";
import { useGetTableData } from "hooks/useGetTableData";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import NumberFilterSelect from "./NumberFilterSelect";

const NumberFilter = () => {
  const { refetch, query, isInvoices, findOne } = useGetTableData();

  const { t } = useTranslation();

  const [Number, setNumber] = useState("");
  const handleInputChange = (e) => setNumber(e.target.value);

  **const [label, setLabel] = useState(t("invoice.lookup.labelinvoiceNo"));**
  const handleLabelChange = (label) => setLabel(label);

  const handleFilterByNumber = () => {
    if (Number === "") return;
    findOne({
      customerId: query.customerId,
      [`${isInvoices ? "invoiceNumber" : "orderNumber"}`]: Number,
    });
  };

  const clearNumberFilter = () => {
    refetch();
    setNumber("");
  };

  return (
    <Box
      sx={(theme) => ({
        display: "flex",
        backgroundColor: theme.palette.inputBackgroundColor.main,
        border: `1px solid ${theme.palette.inputBorderColor.main}`,
        borderRadius: "4px",
        "&:hover": {
          background: "#e7e7e7",
        },
      })}
    >
      <TextField
        label={label}
        value={Number}
        onChange={handleInputChange}
        variant="filled"
        onKeyDown={(e) => e.key === "Enter" && handleFilterByNumber()}
        sx={{
          width: "100%",
          "& .MuiInputBase-root": {
            background: "none",
            border: "none",
          },
        }}
        InputLabelProps={{ shrink: true }}
        InputProps={{
          endAdornment: (
            <IconButton onClick={handleFilterByNumber}>
              <SearchIcon />
            </IconButton>
          ),
          disableUnderline: true,
          sx: {
            background: "none",
            borderRadius: 0,
            "&:hover": {
              background: "none",
            },
          },
        }}
      />
      {isInvoices && (
        <NumberFilterSelect handleLabelChange={handleLabelChange} />
      )}
      <Box
        sx={(theme) => ({
          display: "flex",
          borderLeft: `1px solid ${theme.palette.inputBorderColor.main}`,
        })}
      >
        <IconButton
          onClick={clearNumberFilter}
          sx={{
            "&:hover": {
              background: "none",
            },
          }}
        >
          <CloseIcon />
        </IconButton>
      </Box>
    </Box>
  );
};

export default NumberFilter;

Solution

  • To quote the docs on the initialState for useState()

    This argument is ignored after the initial render.

    which can be a good thing - if React updated your state each time it rerendered to be the new initialState then you wouldn't be able to set a new state value as it would keep getting reset. So even if t("invoice.lookup.labelinvoiceNo") changes, your state won't reflect that new value. The only way to persistently update your state is through using the state setter useState() returns.

    Instead, update your state to hold your translation key:

    const [labelKey, setLabelKey] = useState("invoice.lookup.labelinvoiceNo");
    

    Then grab the translation when you render:

    <TextField label={t(labelKey)} ... />
    

    this will also mean that NumberFilterSelect should provide a translation key rather than a raw label to its handleLabelChange callback.