简体   繁体   中英

Object's initial data getting reset on updating state using useState hook

I'm trying to implement Drag and Drop component offered by react-beautiful-dnd, for mapping courses to a program from a given set of courses available on the database. DND(Drag&Drop) component has 2 columns - Mapped Courses & Available Courses, the data of which is fetched from API. For accomplishing this, I'm make an initialData object, populating it with Mapped Courses & Available Courses fetched from DB, then finally copying this initialData to the columns of DND Component. Problem - When updating initialData object with 'Mapped Courses', 'Available Courses' is getting reset, and while updating 'Available Courses', 'Mapped Courses' is getting reset.

Code -

const MainComponent=()=>{

 //initialData containing courses fetched from DB
 const [initialData, setInitialData] = useState({
    ['Mapped Courses']: {
        name: 'Mapped Courses',
        items: []
    },
    ['Available Courses']: {
        name: 'Available Courses',
        items: []
    }
 });

 //columns of react DnD containing courses after they've been fetched
 const [columns, setColumns] = useState({
     ['Mapped Courses']: {
         name: 'Mapped Courses',
         items: []
     },
     ['Available Courses']: {
         name: 'Available Courses',
         items: []
     }
 });

//react DnD fn for making changes after drag
const onDragEnd = (result, columns, setColumns) => {
    if (!result.destination) return;
    const { source, destination } = result;

    if (source.droppableId !== destination.droppableId) {
        const sourceColumn = columns[source.droppableId];
        const destColumn = columns[destination.droppableId];
        const sourceItems = [...sourceColumn.items];
        const destItems = [...destColumn.items];
        const [removed] = sourceItems.splice(source.index, 1);
        destItems.splice(destination.index, 0, removed);
        setColumns({
            ...columns,
            [source.droppableId]: {
                ...sourceColumn,
                items: sourceItems
            },
            [destination.droppableId]: {
                ...destColumn,
                items: destItems
            }
        });
        console.log(columns);
    } else {
        const column = columns[source.droppableId];
        const copiedItems = [...column.items];
        const [removed] = copiedItems.splice(source.index, 1);
        copiedItems.splice(destination.index, 0, removed);
        setColumns({
            ...columns,
            [source.droppableId]: {
                ...column,
                items: copiedItems
            }
        });
        console.log(columns);
    }
};

useEffect(async()=>{
axios
    .get(`http://programsAPI/program`)
    .then((response) =>
        setInitialData({
            ...initialData,
            ['Mapped Courses']: {
                ...initialData['Mapped Courses'],
                items: response.data.courses.map((course) => ({ id: course.id, content: course.slug }))
            }
        })
    )
    .then(console.log(`Initial Data after Mapping Courses - `), console.log(initialData))
    .then(
        axios
            .get('http://coursesAPI/courses')
            .then((response) =>
                setInitialData({
                    ...initialData,
                    ['Available Courses']: {
                        ...initialData['Available Courses'],
                        items: response.data.map((course) => ({ id: course._id, content: course.info.slug }))
                    }
                })
            )
            .then(console.log('Initial Data after Getting Courses - '), console.log(initialData))
            .catch((err) => console.log(err))
    )
    .catch((err) => console.log(err))
)
}, [programLoads])

useEffect(() => {
    setColumns({
        ...columns,
        ['Mapped Courses']: {
            ...columns['Mapped Courses'],
            items: initialData['Mapped Courses'].items
        },
        ['Available Courses']: {
            ...columns['Available Courses'],
            items: initialData['Available Courses'].items
        }
    });
    console.log('Initial Data after setting columns/update');
    console.log(initialData);
}, [initialData]);

return ( 
    //HTML STARTS HERE
)
}

It seems the issue is more to do with enqueueing more than a single update to the same state within a render cycle's scope. In other words, in the useEffect callback you are enqueueing two state updates that are both updating from the stale enclosure of state.

You can remedy this by using a functional state update so each individual update is applied against the previous state and not the state value closed over in scope.

You are also declaring the useEffect callback as async which isn't a correct use of the hook as the callback implicitly returns a Promise which React interprets as a returned effect cleanup function. It's also anti-pattern to declare a function async and then use Promise chains instead of awaiting promises to resolve.

Promise chains were also designed/meant to solve a problem of callback nesting, you can return the second axios Promise and flatten the Promise chain and use a single catch .

useEffect(() => {
  const courseMapFn = (course) => ({
    id: course._id,
    content: course.info.slug,
  });

  axios
    .get(`http://programsAPI/program`)
    .then((response) =>
      setInitialData(initialData => ({ // <-- previous state
        ...initialData,
        ['Mapped Courses']: {
          ...initialData['Mapped Courses'],
          items: response.data.courses.map(courseMapFn),
        },
      }))
    )
    .then(() => axios.get('http://coursesAPI/courses'))
    .then((response) =>
      setInitialData(initialData => ({ // <-- previous state
        ...initialData,
        ['Available Courses']: {
          ...initialData['Available Courses'],
          items: response.data.map(courseMapFn),
        },
      }))
    )
    .catch((err) => console.log(err))
  )
}, [programLoads]);

This will enqueue two state updates though, an improvement may be to use an async function and await both GET requests to resolve and map and enqueue a single state update.

useEffect(() => {
  const courseMapFn = (course) => ({
    id: course._id,
    content: course.info.slug,
  });

  const fetchInitialData = async () => {
    try {
      const mappedCourseData = await axios.get(`http://programsAPI/program`);
      const availableCourseData = await axios.get('http://coursesAPI/courses');

      setInitialData(initialData => ({ // <-- previous state
        ...initialData,
        ['Mapped Courses']: {
          ...initialData['Mapped Courses'],
          items: mappedCourseData.data.courses.map(courseMapFn),
        },
       ['Available Courses']: {
          ...initialData['Available Courses'],
          items: availableCourseData.data.map(courseMapFn),
        },,
      }));
    } catch(error) {
      console.error(error);
    }

    fetchInitialData(); // <-- invoke async funciton
}, [programLoads]);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM