i got a problem while working with mui data-grid and socket.io.
I load initial data from an api which works fine.
But when I want to update the table with data from an websocket is only one element added and overwritten on every change. The code is the following:
Table:
const [tableData, setTableData] = useState(props.tableData);
useEffect(() => {
setTableData(props.tableData);
}, [props.tableData]);
return (<div key="tradingTableGrid" style={{ height: 450, width: "100%" }}>
<DataGrid
key={"TradingTable"}
rows={Object.values(tableData)}
columns={tableHead}
rowHeight={30}
pageSize={10}
onRowClick={(row) => openModal(row)}
/>
</div>);
The component where the initial data are loaded and the websocket update is called is the following (i have only added the essential code):
const [data, setData] = useState({});
const [loadData, setLoadData] = useState(false);
const [socketActive, setSocketActive] = useState(false);
// initial data loading from api
const handleResponse = (response) => {
var result = {};
response.forEach((prop) => {
result = {
...result,
[prop.order_id]: {
id: prop.order_id,
orderId: prop.order_id,
price: prop.price.toString(),
volume: prop.max_amount_currency_to_trade,
minVolume: prop.min_amount_currency_to_trade,
buyVolume: prop.max_volume_currency_to_pay + " €",
minBuyVolume: prop.min_volume_currency_to_pay + " €",
user: prop.trading_partner_information.username,
},
};
});
setData(result);
};
// add from websocket
const addOrder = (order) => {
if (
order.trading_pair === "btceur" &&
order.order_type === "buy" &&
(order.payment_option === "1" || order.payment_option === "3")
) {
if (typeof data[order.order_id] === "undefined") {
console.log(data);
setData({
...data,
[order.order_id]: {
id: order.order_id,
orderId: order.order_id,
price: order.price.toString(),
volume: order.amount,
minVolume: order.min_amount,
buyVolume: Number(order.volume).toFixed(2) + " €",
minBuyVolume: Number(order.min_amount * order.price).toFixed(2) + " €",
user: "not set through socket",
},
});
}
}
};
useEffect(() => {
const loadData = () => {
setLoadData(true);
axios
.get(process.env.REACT_APP_BITCOIN_SERVICE + "/order/book?type=buy")
.then((response) => handleResponse(response.data.orders))
.catch((exception) => console.error(exception))
.finally(() => setLoadData(false));
};
if (Object.keys(data).length === 0 && socketActive === false) {
loadData();
}
const socket = () => {
const websocket = io("https://ws-mig.bitcoin.de:443/market", {
path: "/socket.io/1",
});
websocket.on("connect", function () {
setSocketActive(true);
});
websocket.on("remove_order", function (order) {
removeOrder(order);
});
websocket.on("add_order", function (order) {
addOrder(order);
});
websocket.on("disconnect", function () {
setSocketActive(false);
});
};
if (socketActive === false && Object.keys(data).length !== 0) {
socket();
}
}, [data, socketActive]);
return (
<Table
tableHeaderColor="info"
tableHead={tableHead}
tableData={data}
type="buy"
/>
);
Does anyone has an idea why on the update throught the socket only one element in the data is added and on every next call overwritten?
The Problem
The reason is that the socket handlers are using the state setters from the first render.
as an example take addOrder, you create that function within the context of a specific data value, when you call setData({...data it will always use the initial state for data. (to understand why see How do JavaScript closures work? and also A Complete Guide to useEffect from facebook's react developer Dan Abramov)
The Solution
what you can do instead is use the functional update syntax that lets you pass a function to setData:
setData( data => ({...data,
this ensures the last stored value is always used.
see the react docs on useState Functional updates
Related
I am trying to write a program that communicates with the client frequently and I need to quickly notice the changes and read the result from the server. But this request is constantly being sent and loaded, even when the user is not interacting. And this causes the user's system to be occupied.
this is mu code:
const AdminDashboard = () => {
const [filterShow, setFilterShow] = useState({ sort: "", read: "", flag: "", skip: 0, limit: 15 });
const [adminItemList, setAdminItemList] = useState([]);
const [searchParams, setSearchParams] = useSearchParams();
async function changeItem(updateItem) {
// update admin item state
await axios.put("{...}/api/admin", { ... });
}
useEffect(() => {
async function resultItem() {
// get result(admin items)
await axios.get(`{...}/api/admin?${searchParams.toString()}`)
.then((res) => {
setAdminItemList(res.data.data);
}).catch((res) => {
console.log(res)
});
}
resultItem();
})
return (<>
{adminItemList.map((ai, i) => {
return (<div key={i}>
<AdminItem item={ai} count={i} skip={filterShow.skip} res={changeItem} />
</div>)
})}
</>);
}
I know that I can avoid resending the request by using "useEffect" and passing an empty array to its second input. But I need to listen the changes so I can't do that.
How can i listening the changes and prevent repeated get requests???
The only solution I could find:
I moved the function "resultItem" outside of useEffect. And I wrote useEffect with an empty array in the second entry. then call "resultItem" in useEffect.
I gave this function "resultItem" an input to receive a query so that it can be flexible
I called it wherever I needed it
Note: In async functions, I first put that function in the Promise and call the "resultItem" in then().
I will put all the written codes here:
const AdminDashboard = () => {
const usePuCtx = useContext(PublicContext);
const { lang, dir } = usePuCtx.language;
// base state
const [allAdminItem, setAllAdminItem] = useState(0);
const [filterShow, setFilterShow] = useState({ sort: "", read: "", flag: "", skip: 0, limit: 3 });
const [adminItemList, setAdminItemList] = useState([]);
// validate for show this page
const [isAdmin, setIsAdmin] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
async function setResalt(radioName, radioState) {
// set setting on query for get result(admin items)
let newFilter = { ...filterShow };
newFilter[radioName] = radioState;
const queryStr = queryString.stringify(newFilter);
setSearchParams(queryStr);
new Promise((res, rej) => { res(setFilterShow(newFilter)) })
.then(() => resultItem(queryStr));
}
async function changeItem(updateItem) {
// update admin item state (read & flag)
await axios.put(usePuCtx.ip_address + "/api/admin",
{ _id: updateItem._id, read: updateItem.read, flag: updateItem.flag })
.then(() => resultItem(searchParams));
}
function showSkipPage(numberPage) {
const nextResult = numberPage * filterShow.limit
setResalt("skip", nextResult)
}
async function resultItem(query) {
// get result(admin items)
await axios.get(usePuCtx.ip_address + `/api/admin?${query.toString()}`)
.then((res) => {
setIsAdmin(true);
setAdminItemList(res.data.data);
}).catch(() => {
// if auth token is wrong
window.location = "/not-found";
});
// get all admin item number
await axios.get(usePuCtx.ip_address + "/api/admin?limit=0&skip=0&flag=&read=&sort=close")
.then((res) => {
setIsAdmin(true);
setAllAdminItem(res.data.data.length);
}).catch(() => {
// if auth token is wrong
window.location = "/not-found";
});
}
useEffect(() => {
resultItem(searchParams);
}, []);
return (!isAdmin ? "" : (<>
<div className="container-fluid" dir={dir}>
<div className="row m-4" dir="ltr">
{/* radio buttons */}
<RadioFilterButton name="read" id1="read-" id2="read-false" id3="read-true"
inner1={txt.radio_filter_button_all[lang]} inner2={txt.radio_filter_button_read_no[lang]}
inner3={txt.radio_filter_button_read[lang]} res={setResalt} />
<RadioFilterButton name="flag" id1="flag-" id2="flag-false" id3="flag-true"
inner1={txt.radio_filter_button_all[lang]} inner2={txt.radio_filter_button_flag_no[lang]}
inner3={txt.radio_filter_button_flag[lang]} res={setResalt} />
<RadioFilterButton name="sort" id1="sort-close" id2="sort-far"
inner1={txt.radio_filter_button_close[lang]} inner2={txt.radio_filter_button_far[lang]}
res={setResalt} />
</div><hr />
<div className="m-4" style={{ minHeight: "100vh" }}>
{
// show result(admin items)
adminItemList.map((ai, i) => {
return (<div key={i}>
<AdminItem item={ai} count={i} skip={filterShow.skip} res={changeItem} />
</div>)
})
}
</div>
<div className="row justify-content-center mt-auto" dir="ltr">
<PageinationTool countAll={allAdminItem} limitCard={filterShow.limit} skipNum={filterShow.skip} res={showSkipPage} />
</div>
<Footer />
</div>
</>));
}
Stackoverflow
problem
I have separate components that house Tiptap Editor tables. At first I had a save button for each Child Component which worked fine, but was not user friendly. I want to have a unified save button that will iterate through each child Table component and funnel all their editor.getJSON() data into an array of sections for the single doc object . Then finish it off by saving the whole object to PouchDB
What did I try?
link to the repo → wchorski/Next-Planner: a CRM for planning events built on NextJS (github.com)
Try #1
I tried to use the useRef hook and the useImperativeHandle to call and return the editor.getJSON(). But working with an Array Ref went over my head. I'll post some code of what I was going for
// Parent.jsx
const childrenRef = useRef([]);
childrenRef.current = []
const handleRef = (el) => {
if(el && !childrenRef.current.includes(el)){
childrenRef.current.push(el)
}
}
useEffect(() =>{
childrenRef.current[0].childFunction1() // I know this doesn't work, because this is where I gave up
})
// Child.jsx
useImperativeHandle(ref, () => ({
childFunction1() {
console.log('child function 1 called');
},
childFunction2() {
console.log('child function 2 called');
},
}))
Try #2
I set a state counter and passed it down as a prop to the Child Component . Then I update the counter to trigger a child function
// Parent.jsx
export const Planner = ({id, doc, rev, getById, handleSave, db, alive, error}) => {
const [saveCount, setSaveCount] = useState(0)
const handleUpdate = () =>{
setSaveCount(prev => prev + 1)
}
const isSections = () => {
if(sectionsState[0]) handleSave(sectionsState)
if(sectionsState[0] === undefined) console.log('sec 0 is undefined', sectionsState)
}
function updateSections(newSec) {
setsectionsState(prev => {
const newState = sectionsState.map(obj => {
if(!obj) return
if (obj.header === newSec.header) {
return {...obj, ...newSec}
}
// 👇️ otherwise return object as is
return obj;
});
console.log('newState', newState);
return newState;
});
}
useEffect(() => {
setsectionsState(doc.sections)
}, [doc])
return (<>
<button
title='save'
className='save'
onPointerUp={handleUpdate}>
Save to State <FiSave />
</button>
<button
style={{right: "0", width: 'auto'}}
title='save'
className='save'
onClick={isSections}>
Save to DB <FiSave />
</button>
{doc.sections.map((sec, i) => {
if(!sec) return
return (
<TiptapTable
key={i}
id={id}
rev={doc.rev}
getById={getById}
updateSections={updateSections}
saveCount={saveCount}
section={sec}
db={db}
alive={alive}
error={error}
/>
)
})}
</>)
// Child.jsx
export const TiptapTable = ((props, ref) => {
const {id, section, updateSections, saveCount} = props
const [currTimeStart, setTimeStart] = useState()
const [defTemplate, setdefTemplate] = useState('<p>loading<p>')
const [isLoaded, setIsLoaded] = useState(false)
const [notesState, setnotesState] = useState('')
const editor = useEditor({
extensions: [
History,
Document,
Paragraph,
Text,
Gapcursor,
Table.configure({
resizable: true,
}),
TableRow.extend({
content: '(tableCell | tableHeader)*',
}),
TableHeader,
TableCell,
],
// i wish it was this easy
content: (section.data) ? section.data : defTemplate,
}, [])
const pickTemplate = async (name) => {
try{
const res = await fetch(`/templates/${name}.json`,{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json()
setIsLoaded(true)
setdefTemplate(data)
console.log('defTemplate, ', defTemplate);
// return data
} catch (err){
console.warn('template error: ', err);
}
}
function saveData(){
console.log(' **** SAVE MEEEE ', section.header);
try{
const newSection = {
header: section.header,
timeStart: currTimeStart,
notes: notesState,
data: editor.getJSON(),
}
updateSections(newSection)
} catch (err){
console.warn('table update error: ', id, err);
}
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) saveData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
useEffect(() => {
setTimeStart(section.timeStart)
setnotesState(section.notes)
if(!section.data) pickTemplate(section.header).catch(console.warn)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, section, isLoaded])
useEffect(() => {
if (editor && !editor.isDestroyed) {
if(section.data) editor.chain().focus().setContent(section.data).run()
if(!section.data) editor.chain().focus().setContent(defTemplate).run()
setIsLoaded(true)
}
}, [section, defTemplate, editor]);
if (!editor) {
return null
}
return isLoaded ? (<>
<StyledTableEditor>
<div className="title">
<input type="time" label='Start Time' className='time'
onChange={(e) => setTimeStart(e.target.value)}
defaultValue={currTimeStart}
/>
<h2>{section.header}</h2>
</div>
<EditorContent editor={editor} className="tiptap-table" ></EditorContent>
// ... non relavent editor controls
<button
title='save'
className='save2'
onPointerUp={() => saveData()}>
Save <FiSave />
</button>
</div>
</nav>
</StyledTableEditor>
</>)
: null
})
TiptapTable.displayName = 'MyTiptapTable';
What I Expected
What I expected was the parent state to update in place, but instead it overwrites the previous tables. Also, once it writes to PouchDB it doesn't write a single piece of new data, just resolved back to the previous, yet with an updated _rev revision number.
In theory I think i'd prefer the useRef hook with useImperativeHandle to pass up the data from child to parent.
It looks like this question is similar but doesn't programmatically comb through the children
I realize I could have asked a more refined question, but instead of starting a new question I'll just answer my own question from what I've learned.
The problem being
I wasn't utilizing React's setState hook as I iterated and updated the main Doc Object
Thanks to this article for helping me through this problem.
// Parent.jsx
import React, {useState} from 'react'
import { Child } from '../components/Child'
export const Parent = () => {
const masterDoc = {
_id: "123",
date: "2023-12-1",
sections: [
{header: 'green', status: 'old'},
{header: 'cyan', status: 'old'},
{header: 'purple', status: 'old'},
]
}
const [saveCount, setSaveCount] = useState(0)
const [sectionsState, setsectionsState] = useState(masterDoc.sections)
function updateSections(inputObj) {
setsectionsState(prev => {
const newState = prev.map(obj => {
// 👇️ if id equals 2, update country property
if (obj.header === inputObj.header)
return {...obj, ...inputObj}
return obj;
});
return newState;
});
}
return (<>
<h1>Parent</h1>
{sectionsState.map((sec, i) => {
if(!sec) return
return (
<Child
key={i}
section={sec}
updateSections={updateSections}
saveCount={saveCount}
/>
)
})}
<button
onClick={() => setSaveCount(prev => prev + 1)}
>State dependant update {saveCount}</button>
</>)
}
// Child.jsx
import React, {useEffect, useState, forwardRef, useImperativeHandle} from 'react'
export const Child = forwardRef((props, ref) => {
const {section, updateSections, saveCount} = props
const [statusState, setStatusState] = useState(section.status)
function modData() {
const obj = {
header: section.header,
status: statusState
}
updateSections(obj)
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) modData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
return (<>
<span style={{color: section.header}}>
header: {section.header}
</span>
<span>status: {section.status}</span>
<input
defaultValue={section.status}
onChange={(e) => setStatusState(e.target.value)}
/>
________________________________________
</>)
})
Child.displayName = 'MyChild';
Requirement
I have a requirement to get the editor state in JSON format as well as the text content of the editor. In addition, I want to receive these values in the debounced way.
I wanted to get these values (as debounced) because I wanted to send them to my server.
Dependencies
"react": "^18.2.0",
"lexical": "^0.3.8",
"#lexical/react": "^0.3.8",
You don't need to touch any of Lexical's internals for this; a custom hook that reads and "stashes" the editor state into a ref and sets up a debounced callback (via use-debounce here, but you can use whatever implementation you like) is enough.
getEditorState is in charge of converting the editor state into whichever format you want to send over the wire. It's always called within editorState.read().
function useDebouncedLexicalOnChange<T>(
getEditorState: (editorState: EditorState) => T,
callback: (value: T) => void,
delay: number
) {
const lastPayloadRef = React.useRef<T | null>(null);
const callbackRef = React.useRef<(arg: T) => void | null>(callback);
React.useEffect(() => {
callbackRef.current = callback;
}, [callback]);
const callCallbackWithLastPayload = React.useCallback(() => {
if (lastPayloadRef.current) {
callbackRef.current?.(lastPayloadRef.current);
}
}, []);
const call = useDebouncedCallback(callCallbackWithLastPayload, delay);
const onChange = React.useCallback(
(editorState) => {
editorState.read(() => {
lastPayloadRef.current = getEditorState(editorState);
call();
});
},
[call, getEditorState]
);
return onChange;
}
// ...
const getEditorState = (editorState: EditorState) => ({
text: $getRoot().getTextContent(false),
stateJson: JSON.stringify(editorState)
});
function App() {
const debouncedOnChange = React.useCallback((value) => {
console.log(new Date(), value);
// TODO: send to server
}, []);
const onChange = useDebouncedLexicalOnChange(
getEditorState,
debouncedOnChange,
1000
);
// ...
<OnChangePlugin onChange={onChange} />
}
Code
File: onChangeDebouce.tsx
import {$getRoot} from "lexical";
import { useLexicalComposerContext } from "#lexical/react/LexicalComposerContext";
import React from "react";
const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';
const useLayoutEffectImpl = CAN_USE_DOM ? React.useLayoutEffect : React.useEffect;
var useLayoutEffect = useLayoutEffectImpl;
type onChangeFunction = (editorStateJson: string, editorText: string) => void;
export const OnChangeDebounce: React.FC<{
ignoreInitialChange?: boolean;
ignoreSelectionChange?: boolean;
onChange: onChangeFunction;
wait?: number
}> = ({ ignoreInitialChange= true, ignoreSelectionChange = false, onChange, wait= 167 }) => {
const [editor] = useLexicalComposerContext();
let timerId: NodeJS.Timeout | null = null;
useLayoutEffect(() => {
return editor.registerUpdateListener(({
editorState,
dirtyElements,
dirtyLeaves,
prevEditorState
}) => {
if (ignoreSelectionChange && dirtyElements.size === 0 && dirtyLeaves.size === 0) {
return;
}
if (ignoreInitialChange && prevEditorState.isEmpty()) {
return;
}
if(timerId === null) {
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
})
}, wait);
} else {
clearTimeout(timerId);
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
});
}, wait);
}
});
}, [editor, ignoreInitialChange, ignoreSelectionChange, onChange]);
return null;
}
This is the code for the plugin and it is inspired (or copied) from OnChangePlugin of lexical
Since, lexical is in early development the implementation of OnChangePlugin might change. And in fact, there is one more parameter added as of version 0.3.8. You can check the latest code at github.
The only thing I have added is calling onChange function in timer logic.
ie.
if(timerId === null) {
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
})
}, wait);
} else {
clearTimeout(timerId);
timerId = setTimeout(() => {
editorState.read(() => {
const root = $getRoot();
onChange(JSON.stringify(editorState), root.getTextContent());
});
}, wait);
}
If you are new to lexical, then you have to use declare this plugin as a child of lexical composer, something like this.
File: RichEditor.tsx
<LexicalComposer initialConfig={getRichTextConfig(namespace)}>
<div className="editor-shell lg:m-2" ref={scrollRef}>
<div className="editor-container">
{/* your other plugins */}
<RichTextPlugin
contentEditable={<ContentEditable
className={"ContentEditable__root"} />
}
placeholder={<Placeholder text={placeHolderText} />}
/>
<OnChangeDebounce onChange={onChange} />
</div>
</div>
</LexicalComposer>
In this code, as you can see I have passed the onChange function as a prop and you can also pass wait in milliseconds like this.
<OnChangeDebounce onChange={onChange} wait={1000}/>
Now the last bit is the implementation of onChange function, which is pretty straightforward
const onChange = (editorStateJson:string, editorText:string) => {
console.log("editorStateJson:", editorStateJson);
console.log("editorText:", editorText);
// send data to a server or to your data store (eg. redux)
};
Finally
Thanks to Meta and the lexical team for open sourcing this library. And lastly, the code I have provided works for me, I am no expert, feel free to comment to suggest an improvement.
I am trying to access the res.data.id from a nested axios.post call and assign it to 'activeId' variable. I am calling the handleSaveAll() function on a button Click event. When the button is clicked, When I console the 'res.data.Id', its returning the value properly, but when I console the 'activeId', it's returning null, which means the 'res.data.id' cannot be assigned. Does anyone have a solution? Thanks in advance
const [activeId, setActiveId] = useState(null);
useEffect(() => {}, [activeId]);
const save1 = () => {
axios.get(api1, getDefaultHeaders())
.then(() => {
const data = {item1: item1,};
axios.post(api2, data, getDefaultHeaders()).then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
});
});
};
const save2 = () => {
console.log(activeId); // result: null
};
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
return (
<button type='submit' onClick={handleSaveAll}>Save</button>
);
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, like in your example, the console.log function runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(activeId);
}, [activeId);
The callback will run every time the state value changes and only after it has finished changing and a render has occurred.
Edit:
Based on the discussion in the comments.
const handleSaveSections = () => {
// ... Your logic with the `setState` at the end.
}
useEffect(() => {
if (activeId === null) {
return;
}
save2(); // ( or any other function / logic you need )
}, [activeId]);
return (
<button onClick={handleSaveSections}>Click me!</button>
)
As the setState is a async task, you will not see the changes directly.
If you want to see the changes after the axios call, you can use the following code :
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
setTimeout(()=>console.log(activeId),0);
})
useEffect(() => {
}, [activeId]);
const [activeId, setActiveId] = useState(null);
const save1 = () => {
const handleSaveSections = async () => {
activeMetric &&
axios.get(api1, getDefaultHeaders()).then(res => {
if (res.data.length > 0) {
Swal.fire({
text: 'Record already exists',
icon: 'error',
});
return false;
}
else {
const data = {
item1: item1,
item2: item2
}
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
})
}
});
}
handleSaveSections()
}
const save2 = () => {
console.log(activeId); //correct result would be shown here
}
const handleSaveAll = () => {
save1();
save2();
}
return (
<button type="submit" onClick={handleSaveAll}>Save</button>
)
I am attempting to develop a React app which makes a call to a database to load a set of pages to a board to build a drag and drop decision tree.
I am only just starting out with React, so keen to hear about anything I'm doing wrong here.
Using 'useEffect' the pageTree function will load the pages up on the first load and on every refresh, however the pages state returns with an empty array instead of the current pages.
Strangely enough the pages all show up on the board with the pages.map function which works on the pages state... (which returns as empty on console.log...)
If I add a page to the array it saves the change to the database, but then will only show the new page on the board. You will then have to refresh to see the new set of pages (including the added page).
Calls to add or delete a page are called by the layout menu buttons in the parent component.
Console after refresh
Additionally, if I move a page, the state will console OK:
Page state in console after moving a page. DB call and state update works OK
function PageTree({AddNewPageFunc}) {
const [pages, setPages] = useState([]);
const movePage = useCallback((droppedPage) => {
const updatedPages = pages.map(page => droppedPage._id == page._id ? droppedPage : page);
setPages(updatedPages);
}, [pages]);
const [{isOver}, drop] = useDrop(() => ({
accept: ItemTypes.PAGECARD,
drop(page, monitor) {
const delta = monitor.getDifferenceFromInitialOffset();
let x = Math.round(page.x + delta.x);
let y = Math.round(page.y + delta.y);
page.x = x;
page.y = y;
movePage(page);
setNewPagePosition(page);
return undefined;
},
}), [movePage]);
const setNewPagePosition = async (pageDetails) => {
console.log("function called to update page position");
Api.withToken().post('/pageupdate/'+pageDetails._id,
pageDetails
).then(function (response) {
console.log("moved page: ",response.data)
}).catch(function (error) {
//console.log(error);
});
}
React.useEffect(() => {
AddNewPageFunc.current = AddNewPage
}, [])
const AddNewPage = useCallback(() => {
console.log("calling add new page function")
console.log("the pages before the API call are ",pages)
Api.withToken().post('/addblankpage/'
).then(function (response) {
console.log("produced: ",response.data);
setPages(pages.concat(response.data))
console.log("the pages after updating state are: ",pages)
}).catch(function (error) {
//console.log(error);
});
}, [pages]);
const handleDelete = async (id) => {
Api.withToken().post('/deletepages/'+id
).then(function (response) {
let index = pages.findIndex(function(item){
return item.id === response.data._id;
});
const PageRemoved = pages.splice(index, 1);
setPages(PageRemoved);
}).catch(function (error) {
//console.log(error);
});
}
useEffect(() => {
Api.withToken().get('/pages/')
.then(res => {
setPages(res.data);
console.log('res data ',res.data);
console.log('pages ',pages);
})
}, []);
return (
<div ref={drop} style={styles}>
{pages.map((page) => (<PageCard page={page} id={page._id} key={page._id} handleDelete={() => handleDelete(page._id)} handleMaximise={() => handleMaximise(page)} handleCopy={() => handleCopy(page)}/>))}
</div>
)
}
export default PageTree;
As Danielprabhakaran pointed out, the issue was the callback in React.useEffect. On adding a new page it needed to send the updated page state back to the parent component.
Using console.log on a state after an API call seems to be fraught, even if using .then(console.log(state)
function PageTree({AddNewPageFunc}) {
const [pages, setPages] = useState([]);
const movePage = useCallback((droppedPage) => {
const updatedPages = pages.map(page => droppedPage._id == page._id ? droppedPage : page);
console.log("updated pages ",updatedPages);
setPages(updatedPages);
console.log("set pages ",pages);
}, [pages]);
const [{isOver}, drop] = useDrop(() => ({
accept: ItemTypes.PAGECARD,
drop(page, monitor) {
const delta = monitor.getDifferenceFromInitialOffset();
let x = Math.round(page.x + delta.x);
let y = Math.round(page.y + delta.y);
page.x = x;
page.y = y;
movePage(page);
setNewPagePosition(page);
return undefined;
},
}), [movePage]);
const setNewPagePosition = async (pageDetails) => {
console.log("function called to update page position");
Api.withToken().post('/pageupdate/'+pageDetails._id,
pageDetails
).then(function (response) {
console.log("?worked ",response)
}).catch(function (error) {
//console.log(error);
});
}
React.useEffect(() => {
AddNewPageFunc.current = AddNewPage
}, [pages])
const AddNewPage = useCallback(() => {
console.log("calling add new page function")
console.log("the pages before the API call are ",pages)
Api.withToken().post('/addblankpage/'
).then(function (response) {
console.log("produced: ",response.data);
setPages(pages.concat(response.data))
console.log("the pages after updating state are: ",pages)
}).catch(function (error) {
//console.log(error);
});
}, [pages]);
const handleDeletedCallback = (deletedIndex) => {
console.log("delete callback fired")
setPages(pages.splice(deletedIndex, 1));
}
useEffect(() => {
Api.withToken().get('/pages/') //can add in a prop to return only a given tree once the app gets bigger
.then(res => {
setPages(res.data);
console.log('res data ',res.data);
console.log('pages ',pages);
})
}, []);
return (
<div ref={drop} style={styles}>
{pages.map((page, index) => (<PageCard page={page} id={page._id} key={page._id} index={index} deleteCallback={handleDeletedCallback} handleMaximise={() => handleMaximise(page)} handleCopy={() => handleCopy(page)}/>))}
</div>
)
}
export default PageTree;