I am trying to get the latest value of history from redux store to pass in a payload. The issue is whenever I submit a query, the history is getting updated and looping with map() inside return() it is showing the latest value but at // CONSOLE HERE inside submitQuery() it is keep showing old values.
export default function Chat() {
const dispatch = useDispatch();
const { customInstructionId, chatId } = useParams()
const { history } = useSelector((state) => state.customData)
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(false)
const handleSubmitQuery = async () => {
if(!query || query === "" || query === null) {
return notification("Query can't be empty.", "error")
}
setLoading(true)
const data = { id: Math.random(), role: 'user', content: query }
setQuery("")
dispatch(addData(data))
const config = { query: [...history, data], key: customInstructionId }
try {
const response = await fetch("http://localhost:8000/api/query/chat", {
method: "post",
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify(config),
});
if (!response.ok || !response.body) {
throw response.statusText;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
const loopRunner = true;
let str = ""
let count = 0
const resId = Math.random()
setLoading(false);
/* eslint-disable no-await-in-loop */
while (loopRunner) {
const { value, done } = await reader.read();
if (done) {
// CONSOLE HERE
handleSaveHistory()
break;
}
const decodedChunk = decoder.decode(value, { stream: true });
if(value[0] === 10){
str += "\n"
} else {
str += decodedChunk
}
if(count === 0){
dispatch(addData({ id: resId, role: 'assistant', content: str }))
} else {
dispatch(updateData({ id: resId, content: str }))
}
count += 1
}
// CONSOLE HERE
} catch (err) {
console.error(err, "err");
} finally {
setLoading(false);
}
}
const handleSaveHistory = async () => {
// CONSOLE HERE
const config = saveHistory({ history, customInstructionId, chatId }, token)
axios(config)
.then((response) => {
console.log(response.data)
})
.catch((error) => {
console.log(error)
notification(`Unable to save the history.`, "error")
})
}
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
handleSubmitQuery();
}
};
return (
<Page title="Query" sx={{ height: '100%' }}>
<Container sx={{ height: '100%' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', height: '100%' }}>
<Box sx={{ overflowY: 'scroll', height: '70vh'}}>
{(history && history.length > 0) ? history.map((item) =>
<Card key={item.id} sx={{ marginBottom: '1rem' }}>
<Typography sx={{ padding: '1rem', backgroundColor: item.role === 'user' ? '#f8eff0' : '#edeff1' }}>
<div dangerouslySetInnerHTML={{ __html: item.content.replace(/\n/g, '<br />') }} />
</Typography>
</Card>
) :
<Card sx={{ marginBottom: '1rem' }}>
<Typography sx={{ padding: '1rem', backgroundColor: '#edeff1' }}>Hi! How can I assist you today?</Typography>
</Card>
}
{loading && loading === true &&
<Card sx={{ marginBottom: '1rem' }}>
<Typography sx={{ padding: '1rem', backgroundColor: '#edeff1' }}>
<Skeleton variant="text" sx={{ fontSize: '1rem' }} />
</Typography>
</Card>
}
</Box>
</Box>
</Container>
</Page>
);
}
Redux Store:
export const counterSlice = createSlice({
name: "customData",
initialState: {
history: [],
boolean: true,
string: ""
},
reducers: {
addData: (state, action) => {
state.history.push(action.payload)
},
updateData: (state, action) => {
state.history = state.history.map(data => {
if (data.id === action.payload.id) {
return {
...data,
content: action.payload.content
};
}
return data;
});
},
removeData: (state, action) => {
state.history = state.history.filter(data => data.id !== action.payload)
},
recentData: (state, action) => {
state.history = action.payload
},
clearData: (state) => {
state.history = []
}
}
});
export const { addData, updateData, removeData, clearData, recentData } = counterSlice.actions;
export default counterSlice.reducer;
Note: It is not the whole code, just showing the context of the issue. If anyone know why is this occuring.
In the below image, history is indeed getting updating with dispatch(), and I am able to see it in the UI (it is coming word by word just like in chatgpt). when the whole response has finished then I am executing the saveHistory function, but in that function it is not taking the most recent question/answer.
History(in UI):
History(in saveHistory func):
UPDATE: The issue is when I call save func inside handleSubmit func. below solved the issue but I know it is not a good practice.
useEffect(() => {
if(ready === true) {
handleSaveHistory()
setReady(false)
}
}, [ready]);
Async work needs to be done outside the component using createAsyncThunk
. The following should go in your Redux store:
const handleSaveHistory = createAsyncThunk(
'customData/handleSaveHistory',
async (customInstructionId, chatId, thunkAPI) => {
// CONSOLE HERE
const config = thunkAPI.dispatch(saveHistory({
history: thunkAPI.getState().history,
customInstructionId,
chatId,
}, token))
axios(config)
.then((response) => {
console.log(response.data)
})
.catch((error) => {
console.log(error)
notification(`Unable to save the history.`, "error")
})
}
);
const fetchUserById = createAsyncThunk(
'customData/handleSubmitQuery',
async (query, customInstructionId, chatId, thunkAPI) => {
if(!query || query === "" || query === null) {
return notification("Query can't be empty.", "error")
}
const data = { id: Math.random(), role: 'user', content: query }
thunkAPI.dispatch(addData(data))
const config = { query: [...history, data], key: customInstructionId }
try {
const response = await fetch("http://localhost:8000/api/query/chat", {
method: "post",
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify(config),
});
if (!response.ok || !response.body) {
throw response.statusText;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
const loopRunner = true;
let str = ""
let count = 0
const resId = Math.random()
/* eslint-disable no-await-in-loop */
while (loopRunner) {
const { value, done } = await reader.read();
if (done) {
// CONSOLE HERE
thunkAPI.dispatch(handleSaveHistory(customInstructionId, chatId))
break;
}
const decodedChunk = decoder.decode(value, { stream: true });
if(value[0] === 10){
str += "\n"
} else {
str += decodedChunk
}
if(count === 0){
thunkAPI.dispatch(addData({ id: resId, role: 'assistant', content: str }))
} else {
thunkAPI.dispatch(updateData({ id: resId, content: str }))
}
count += 1
}
// CONSOLE HERE
} catch (err) {
console.error(err, "err");
}
}
)
The issue is your hook handleSaveHistory
function closed over an old version of the data. You have async
functions that are executing in a previously-rendered version of your component.
initial state:
Chat1 (visible to user)
- history1
- handleSubmitQuery1
- handleSaveHistory1
next state:
(re-rendered after executing `await fetch`)
Chat1 (DEAD) Chat2 (visible to user)
- history1 - history2
- handleSubmitQuery1 (awaiting) - handleSubmitQuery2
- handleSaveHistory1 - handleSaveHistory2
now the fetch
returns in the old, dead Chat1
component
next state:
Chat1 (DEAD) Chat2 (visible to user)
- history1 - history2
- handleSubmitQuery1 - handleSubmitQuery2
- handleSaveHistory1 (console!) - handleSaveHistory2