In my React 17.0.1 SPA translation app I have a page with 2 sections. The top is for input and the bottom is for translations of that input. I have had problems using a single context/state for this page because the translation is performed by a remote API, and when it returns I can't update the UI without updating the input part and resetting it to whatever value it held at the time the translation API was called.
So, I have split the page into two contexts, one for input and one for translations. So far so good. The rendering for the translations works the first time I do it. But, despite the state in the translation-context changing when needed via its reducer (I have logging that proves that) the appropriate part of the page doesn't re-render in step with those changes. As I said, this is odd because the first time I change the state this way it does actually rerender.
This sketch shows the basic structure in more detail, but I have left a lot out:
App.js
import { React, useRef, createContext, useReducer } from "react";
import ReactDOM from 'react-dom';
import { Form } from "react-bootstrap";
import { Translation } from "./Translation";
const NativeContext = createContext();
const TranslationsContext = createContext();
const nativeReducer = (state, action) {...}
const translationsReducer = (state, action) {...}
const App = () {
const inputControl = useRef(null);
const [nativeState, nativeDispatch] = useReducer(nativeReducer, {});
const [translationsState, translationsDispatch] = useReducer(translationsReducer, {});
const handleChange = async () => {
// fetch from external API
translationsDispatch({ type: "TX", payload: { text: text, translations: data.translations } });
}
return (
<NativeContext.Provider value={{ nativeState, nativeDispatch }}>
<Form.Control key="fc-in" autoFocus ref={inputControl} as="textarea" id="inputControl" defaultValue={nativeState.text} onChange={textChanged} />
</NativeContext.Provider>
<TranslationsContext.Provider value={{ translationsState, translationsDispatch }}>
{translationsState.translations.map((item, index) => {
return <Translation index={index} item={item} to={item.language} key={`tx-${item.language}`} />;
})}
</TranslationsContext.Provider>
);
}
I have the usual index.js kicking it all off:
index.js
ReactDOM.render(<React.StrictMode><App /></React.StrictMode>,document.getElementById('root'));
The input control is so that I can get the current value from the input to translate. I have a 500ms timer-based denounce in there that makes using the updated react state way too hard for that purpose. It isn't relevant to this problem though.
The reducers, not shown are both of the form:
const nativeReducer = (state, action) => {
switch (action.type) {
case "TEXT":
return {
...state,
text: action.payload.text
};
}
};
The translations reducer is too big to show here, as it deals with maintaining a cache and other information regarding other UI components not shown here. I also haven't shown the fetch for the API, but I'm sure your imagination can fill that in.
The translation state actually looks like this:
const initialTranslationsState = {
to: ["de"],
translations: [
{
language: "de",
text: "Ich hab keine lust Deutsche zu sprechen"
}
]
};
The Translation component is rather simple:
Translation.js
import { React, useContext } from "react";
export const Translation = props => {
const { translationsState, translationsDispatch } = useContext(TranslationsContext);
return (
<Form.Control as="textarea" defaultValue={translationsState.translations[props.index].text} readOnly />
);
}
The props that are passed in contain data pertaining to which of the translations this particular instance of the component is supposed to reference. I didn't want to pass the translation itself in the props, so only the index is passed in the props. The translation itself is passed in the context.
When I enter the page there is already some text in the NativeContext (that refers to native language btw), and I have a useEffect clause that calls translate to kick the whole thing off. That causes the translations to be updated and after a second or two, I see the screen components rendering the results without updating the input text (which it turns out is very important).
But, when I then go and edit the input text, the translation results aren't rendered. As I mentioned I have logging to check the state, and it has changed appropriately, but the subcomponent that renders them on the page isn't being rendered so I only see the output in the console. This happens no matter how often I update the input text.
I am a bit stuck with this. I have reviewed similar questions on stackoverflow, but they mainly relate to people trying to mutate state, which I am not doing, or are related to the older class-based approach, which was flawed by design anyway. Let me know if you need more info to help work this out.
valueinstead ofdefaultValueonForm.Control?