I am making a website to modify db data. first, The structure of the component is as follows
<Contents /> <Table /> <Row /> <Column /> <Input />
When the row component is created,creating the ref of the input component and redux manages it.
const StyledRow = styled.div` text-align:center; display:flex; align-items:center; `; const DeleteButton = styled(Button)` background-color: #ff7787; margin-right:5px; color:white; width:40px; ${({top}) => top && css` background-color:white; color:white; width:40px; `} `; function Row({top, rowId}){ const dispatch = useDispatch(); const columns = useMemo(() => columnPhoneInfo,[]); const inputsRef = useMemo(()=> !top && Array(8).fill(0).map(() => createRef() ),[]); // const inputsRef = useRef([]); useEffect(()=> { // console.log(rowId,top); !top && dispatch(phoneDataAddRef(rowId,inputsRef)); },[]); const handleDeleteButton = useCallback( (id) => { dispatch(phoneDataUpdate.Delete(id)); },[]); if( top ) return( <StyledRow> <DeleteButton top/> {columns.map((column)=> <Column key={`head_${column.name}`} width={column.width} top> {column.name} </Column> )} </StyledRow> ); return( <StyledRow> <DeleteButton onClick={()=>handleDeleteButton(rowId)}> delete </DeleteButton> {columns.map((column, index)=> <Column key={`row_${rowId}_${column.name}`} width={column.width} textalign={column.textalign}> <Input ref={inputsRef[index] } colIndex={index} id={rowId} column={column} /> {/* <Input colIndex={index} id={rowId} column={column} /> */} </Column> )} </StyledRow> ); } export default React.memo(Row);
Input component only receives ref as forwardRef
const StyledInput = styled.input` ${({ width, textalign })=>css` width:${width}; text-align:${textalign}; `} `; const Input = forwardRef(({colIndex, id},inputRef) =>{ const dispatch = useDispatch(); const didShowAlert = useRef(false); const nowColumnInfo = columnPhoneInfo[colIndex]; const nowColumnValidCheck = inputValidCheck[colIndex]; const { nowVal, firstVal, isAddedRow } = useSelector(state =>({ nowVal : state.phoneData.data.rows.find(val=>val.id === id)[nowColumnInfo.colname], firstVal : state.phoneData.firstData.lastId < id ? null : state.phoneData.firstData.rows.find(val=>val.id===id)[nowColumnInfo.colname], isAddedRow : state.phoneData.firstData.lastId < id ? true : false, }),shallowEqual); const callbackDispatch = useCallback((dispatchFunc) =>{ return(...args)=>{ dispatch(dispatchFunc(...args)); } },[dispatch]); ////////////////////// const inputChange = useCallback( (value) => dispatch(phoneDataUpdate.Change(id,nowColumnInfo.colname, value)) ,[nowColumnInfo.colname, dispatch, id]); ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const updateListChange = callbackDispatch(phoneDataUpdateList.Change); const updateListDelete = callbackDispatch(phoneDataUpdateList.Delete); ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const handleChange = useCallback( (e) => { // ... todo handle change },[]); ///////////////////////////////////////////////////////// const handleBlur = useCallback( (e) =>{ // ... todo handle blur },[]); return( <StyledInput textalign={nowColumnInfo.textalign} width={nowColumnInfo.width} value={nowVal === null ? '': nowVal } onChange={handleChange} onBlur={handleBlur} ref={inputRef} // placeholder={} /> ); }); export default React.memo(Input);
And finally, the redux module
//////////////////////////////////////////////////////// const PHONE_DATA_DELETE = 'phoneData/PHONE_DATA_DELETE'; //////////////////////////////////////////////////////// const PHONE_DATA_ADD_REF = 'phoneData/PHONE_DATA_ADD_REF'; //////////////////////////////////////////////////////// const dataInitRow = { id:null, model_name:null, machine_name:null, shipping_price:null, maker:null, created:null, battery:null, screen_size:null, storage:null, }; const dataInit = { lastId:null, rows:[], } const initialState = { state:{ loading:false, error:false, }, data:dataInit, refData:[], firstData:dataInit, dataChangeList:{ dataAddList:[], dataDeleteList:[], dataUpdateList:[], }, }; const phoneDataFetchAsync = createPromiseThunk(PHONE_DATA, restAPI.getAllPhoneInfo); //////////////////////////////////////////////////////// const phoneDataAddRef=(id, ref) =>({ type:PHONE_DATA_ADD_REF, id:id, ref:ref, }); const phoneDataUpdateList = ({ Change:(id,colName, value) => ({ type:PHONE_DATA_UPDATE_LIST_CHANGE, id: id, colName: colName, value: value, }), Delete:(id, colName) => ({ type:PHONE_DATA_UPDATE_LIST_DELETE, id: id, }), }); //////////////////////////////////////////////////////// export default function phoneData(state = initialState, action){ // console.log(`add: ${state.dataChangeList.dataAddList}, delete: ${state.dataChangeList.dataDeleteList}, change: ${state.dataChangeList.dataUpdateList}`); switch(action.type) case PHONE_DATA_DELETE: return produce(state, draft=>{ console.log(action); const idx = state.dataChangeList.dataAddList.findIndex( val => val === action.id); if( idx === -1 ) draft.dataChangeList.dataDeleteList.push(action.id); else draft.dataChangeList.dataAddList.splice(idx,1); draft.refData = state.refData.filter(row => row.id !== action.id); draft.data.rows = state.data.rows.filter(row =>row.id !== action.id); }); //////////////////////////////////////////////////////////////////////////////////////////////////////////// case PHONE_DATA_ADD_REF: return produce(state, draft=>{ draft.refData.push({id:action.id, refs:action.ref}); }); //////////////////////////////////////////////////////////////////////////////////////////////////////////// default: return state; } } export {phoneDataFetchAsync, phoneDataDelete,, phoneDataAddRef, };
The problem area is the delete button. When I press the button, that error comes out. But if don’t add ref to state, no error occurs. or even if I comment out the bottom part, there is no error.
draft.data.rows = state.data.rows.filter(row =>row.id !== action.id);
Or comment out the bottom part
draft.refData.push({id:action.id, refs:action.ref});
I’ve been trying to fix it all day today, but I don’t know what’s wrong. How can I fix it?
Advertisement
Answer
Don’t Store Refs in Redux
What you are attempting to do here violates not one but two of the “Essential” rules of Redux.
Ref objects are mutable by design. The reference changes by updating the .current
property of the ref object, but the object instance stays the same. This violates Redux rules that state must be immutable because the Redux state can be mutated by unrelated React code.
All data in your Redux store should be able to be converted to a JSON string
and back. Anything that would get lost in that conversion does not belong in Redux. An instance of a DOM element is not serializable.
You need to figure out what raw data is needed to represent your state. Store that data in Redux and use it to generate the correct DOM elements. Storing the DOM in Redux is doing it backwards.