Is it possible to BehaviorSubject reset my useState value? - rxjs

Problem: Whenever I click 't' key my theme changed as I expected. But for some reason this cause isModalVisible state reset. This cause one problem - When I have my modal open and click t - the modal disapear. I don't know why. Maybe BehaviorSubject cause the problem? Additionally, if I create another useState for test, which initial value is string 'AAA', and on open modal I set this state to string 'BBB'. Then I click 't' to change theme this test useState back to beginning value 'AAA'
My code looks like this:
const Component1 = () => {
const [mode, setMode] = useRecoilState(selectedModeState);
const { switcher, status } = useThemeSwitcher();
const toggleMode = (newTheme: MODE_TYPE) => {
theme$.next({ theme: newTheme });
localStorage.setItem('theme', newTheme);
};
useEffect(() => {
const mode_sub = theme$.subscribe(({ theme }) => {
switcher({ theme: theme });
setMode(theme);
});
return () => {
mode_sub.unsubscribe();
};
}, []);
return <Component2 toggleMode={toggleMode} currentMode={mode} />
}
const Component2 = ({toggleMode, currentMode}) => {
const [mode, setMode] = useState(currentMode);
const [savedTheme, setSavedTheme] = useRecoilState(selectedThemeState);
const getTheme = mode === MODE_TYPE.LIGHT ? MODE_TYPE.DARK : MODE_TYPE.LIGHT;
const themeListener = (event: KeyboardEvent) => {
switch (event.key) {
case 't':
setMode(getTheme);
toggleMode(getTheme);
break;
}
};
useEffect(() => {
document.addEventListener('keydown', themeListener);
document.body.setAttribute('data-theme', savedTheme);
localStorage.setItem('color', savedTheme);
return () => {
document.removeEventListener('keydown', themeListener);
};
}, [savedTheme]);
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
return (
<Modal isModalVisible={isModalVisible} setIsModalVisible={setIsModalVisible} />
)
}
UTILS:
export const selectedModeState = atom<MODE_TYPE>({
key: 'selectedThemeState',
default: (localStorage.getItem('theme') as MODE_TYPE) || MODE_TYPE.LIGHT,
});
export const selectedThemeState = atom<string>({
key: 'selectedColorState',
default: (localStorage.getItem('color') as string) || 'blue',
});
export const theme$ = new BehaviorSubject({
theme: (localStorage.getItem('theme') as THEME_TYPE) || THEME_TYPE.LIGHT,
});
I would like the theme change not to set visibleModal to false which causes the modal to close

Related

Redux RTK reloading state when calling getSelectors

I am new to Redux RTK so the problem might not exactly be on calling getSelectors(). However, when I'm using the state that comes from getSelectors() it reloads the entire state.
Problem
The baseline is that I have different Setup objects that I'm calling based on the documentId. These Setup objects are quite large so in the getSetups I am only fetching some basic properties. Then, when the user selects a specific Setup from the dropdown I want to save it in the setupSlice. But when I trigger the dispatch(setSetup(data)) the RTK reloads all the Setups.
I encounter an infinite loop when after fetching all the Setup objects I want to automatically assign the default Setup to the setupSlice.
Extra
Ideally when I assign a Setup to the setupSlice I would like to call the getSetup from RTK to fetch the entire Setup object of that specific Setup and store it in the setupSlice.
I am not sure if this is suppose to be happening but is there anyway to stop it? Otherwise is there any recommendation so I can move forward?
This is the component I'm trying to generate:
const SetupDropdown = () => {
const dispatch = useDispatch()
const { documentId } = useParams()
const { data, isFetching } = useGetSetupsQuery({ documentId })
let setupsMenu;
const { selectAll: selectAllSetups } = getSelectors({documentId})
const allSetups = useSelector(selectAllSetups)
if (!isFetching) {
const defaultSetup = allSetups.find((setup) => setup.default)
setupsMenu = allSetups.map(setup => {
return (<MenuItem value={setup.id}>{setup.name}</MenuItem>)
})
dispatch(setSetup(defaultSetup))
}
const setupId = useSelector(selectSetupId)
const handleChange = async (event) => {
// Here I ideally call the getSetup RTK Query to fetch the entire information of the single setup
const data = {
id: event.target.value,
name: 'Random name'
}
dispatch(setSetup(data))
};
return (
<FormControl sx={{ minWidth: 200 }} size="small">
<InputLabel>Setup</InputLabel>
<Select
value={setupId}
onChange={handleChange}
label="Setup"
>
{setupsMenu}
</Select>
</FormControl>
)
}
export default SetupDropdown;
This is the setupApiSlice:
const setupsAdapter = createEntityAdapter({
sortComparer: (a, b) => b.date.localeCompare(a.date)
})
const initialState = setupsAdapter.getInitialState()
export const setupsApiSlice = apiSlice.injectEndpoints({
tagTypes: ['Setup'],
endpoints: builder => ({
getSetups: builder.query({
query: ({ documentId }) => ({
url: `/documents/${documentId}/setups`,
method: 'GET'
}),
transformResponse: responseData => {
return setupsAdapter.setAll(initialState, responseData)
},
providesTags: (result, error, arg) => [
{ type: 'Setup', id: "LIST" },
...result.ids.map(id => ({ type: 'Setup', id }))
]
}),
getSetup: builder.query({
query: ({ documentId, setupId }) => ({
url: `/documents/${documentId}/setups/${setupId}`,
method: 'GET'
})
})
})
})
export const {
useGetSetupsQuery,
useGetSetupQuery
} = setupsApiSlice
// Define function to get selectors based on arguments (query) of getSetups
export const getSelectors = (
query,
) => {
const selectSetupsResult = setupsApiSlice.endpoints.getSetups.select(query)
const adapterSelectors = createSelector(
selectSetupsResult,
(result) => setupsAdapter.getSelectors(() => result?.data ?? initialState)
)
return {
selectAll: createSelector(adapterSelectors, (s) =>
s.selectAll(undefined)
),
selectEntities: createSelector(adapterSelectors, (s) =>
s.selectEntities(undefined)
),
selectIds: createSelector(adapterSelectors, (s) =>
s.selectIds(undefined)
),
selectTotal: createSelector(adapterSelectors, (s) =>
s.selectTotal(undefined)
),
selectById: (id) => createSelector(adapterSelectors, (s) =>
s.selectById(s, id)
),
}
}
This is the setupSplice:
const initialState = {
name: null,
filters: [],
data: {},
status: 'idle', //'idle' | 'loading' | 'succeeded' | 'failed'
error: null
}
const setupSlice = createSlice({
name: 'setup',
initialState,
reducers: {
setSetup: (state, action) => {
console.log('Dispatch')
const setup = action.payload;
console.log(setup)
state.id = setup.id;
state.name = setup.name;
state.filters = setup.filters;
state.data = setup.state;
state.status = 'succeeded';
}
}
})
export const { setSetup } = setupSlice.actions;
export const selectSetupId = (state) => state.setup.id;
export const selectSetupName = (state) => state.setup.name;
export const selectSetupFilters = (state) => state.setup.filters;
export const selectSetupData = (state) => state.setup.data;
export default setupSlice.reducer;
Tbh., you probably should be using selectFromResult in your useGetSetupsQuery instead of adding another useSelector hook. That would also reduce your code complexity by a lot.
Your problem as hand is that you are creating those selectors within your component on each render - so they don't have a chance to actually memoize and give you a stable result. If you do that in your component, wrap it in a useMemo call to keep your selector instances as stable as possible.

selector returning different value despite custom equalityFn check returning true

I have the following selector and effect
const filterValues = useSelector<State, string[]>(
state => state.filters.filter(f => f.field === variableId).map(f => f.value),
(left, right) => {
return left.length === right.length && left.every(l => right.includes(l));
},
);
const [value, setValue] = useState<SelectionRange>({ start: null, end: null });
useEffect(() => {
const values = filterValues
.filter(av => av).sort((v1, v2) => v1.localeCompare(v2));
const newValue = {
start: values[0] ?? null,
end: values[1] ?? null,
};
setValue(newValue);
}, [filterValues]);
the selector above initially returns an empty array, but a different one every time and I don't understand why because the equality function should guarantee it doesn't.
That makes the effect trigger, sets the state, the selector runs again (normal) but returns another different empty array! causing the code to run in an endless cycle.
Why is the selector returning a different array each time? what am I missing?
I am using react-redux 7.2.2
react-redux e-runs the selector if the selector is a new reference, because it assumes the code could have changed what it's selecting entirely
https://github.com/reduxjs/react-redux/issues/1654
one solution is to memoize the selector function
const selector = useMemo(() => (state: State) => state.filters.filter(f => f.field === variableId).map(f => f.value), [variableId]);
const filterValues = useSelector<State, string[]>(
selector ,
(left, right) => {
return left.length === right.length && left.every(l => right.includes(l));
},
);
You can try memoizing the result of your filter in a selector and calculate value in a selector as well, now I'm not sure if you still need the local state of value as it's just a copy of a derived value from redux state and only causes an extra render when you copy it but here is the code:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector, defaultMemoize } = Reselect;
const { useState, useEffect, useMemo } = React;
const initialState = {
filters: [
{ field: 1, value: 1 },
{ field: 2, value: 2 },
{ field: 1, value: 3 },
{ field: 2, value: 4 },
],
};
//action types
const TOGGLE = 'NEW_STATE';
const NONE = 'NONE';
//action creators
const toggle = () => ({
type: TOGGLE,
});
const none = () => ({ type: NONE });
const reducer = (state, { type }) => {
if (type === TOGGLE) {
return {
filters: state.filters.map((f) =>
f.field === 1
? { ...f, field: 2 }
: { ...f, field: 1 }
),
};
}
if (type === NONE) {
//create filters again should re run selector
// but not re render
return {
filters: [...state.filters],
};
}
return state;
};
//selectors
const selectFilters = (state) => state.filters;
const createSelectByVariableId = (variableId) => {
const memoArray = defaultMemoize((...args) => args);
return createSelector([selectFilters], (filters) =>
memoArray.apply(
null,
filters
.filter((f) => f.field === variableId)
.map((f) => f.value)
)
);
};
const createSelectSelectValue = (variableId) =>
createSelector(
[createSelectByVariableId(variableId)],
//?? does not work in SO because babel is too old
(values) => ({
start: values[0] || null,
end: values[1] || null,
})
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
var last;
const App = ({ variableId }) => {
const selectValue = useMemo(
() => createSelectSelectValue(variableId),
[variableId]
);
const reduxValue = useSelector(selectValue);
if (last !== reduxValue) {
console.log('not same', last, reduxValue);
last = reduxValue;
}
//not sure if you still need this, you are just
// copying a value you already have
const [value, setValue] = useState(reduxValue);
const dispatch = useDispatch();
useEffect(() => setValue(reduxValue), [reduxValue]);
console.log('rendering...', value);
return (
<div>
<button onClick={() => dispatch(toggle())}>
toggle
</button>
<button onClick={() => dispatch(none())}>none</button>
<pre>{JSON.stringify(value, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App variableId={1} />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>

Why is a state created automatically? (redux)

Redux automatically adds an entry called state. Practice creating counter function. However, the state item that I did not set is automatically added so that it does not appear in the desired direction when dispacth is executed.
I guess it's caused by combineReducers, but I don't know exactly.
Clearly, I didn't make State in counter.
//action name
const INCRE = 'counter/INCRE';
const DECRE = 'counter/DECRE';
export const incre = () => ({type: INCRE});
export const decre = () => ({type: DECRE});
//default
const initState = {
number: 0
};
//reducer
function counter(state = initState, action) {
console.log("frist value");
console.log(state.number);
console.log(initState);
if(action.type === INCRE){
console.log("add");
console.log(state.number)
return {number: state.number +1 }
}else if (action.type === DECRE) {
console.log("minus");
return {number: state.number -1 }
}
else {
return{state};
};
};
export default counter;
// C is containers & Counter is name
import React from 'react';
import Counter from '../components/Counter';
import { connect } from 'react-redux';
import {incre, decre} from '../modules/counter';
const CCounter = ({number, incre, decre}) => {
return (
<Counter number={number} onIncre={incre} onDecre={decre}/>
);
};
const mapStateToProps = state => ({
number: state.counter.number
});
const mapDispatchToProps = dispatch => ({
incre: () => {
dispatch(incre());
},
decre: () => {
dispatch(decre());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(CCounter);
//reducer
//...
else {
return{state};
};
The above line in your reducer is likely the problem because what it can return is what you're seeing:
{
state: {
number: 0,
}
}
You probably wanted to simply return the state.
else {
return state;
};

UI Flickers when I drag and drop and item

I have a problem getting react-beautiful-dnd to work without flickering. I have followed the example in the egghead course. Here is my code sample.
Item List Container
onDragEnd = (result) => {
if (this.droppedOutsideList(result) || this.droppedOnSamePosition(result)) {
return;
}
this.props.itemStore.reorderItem(result);
}
droppedOnSamePosition = ({ destination, source }) => destination.droppableId
=== source.droppableId && destination.index === source.index;
droppedOutsideList = result => !result.destination;
render() {
return (
<DragDropContext onDragEnd={this.onDragEnd}>
<div>
{this.props.categories.map((category, index) => (
<ListCategory
key={index}
category={category}
droppableId={category._id}
/>
))}
</div>
</DragDropContext>
);
}
Item Category
const ListCategory = ({
category, droppableId,
}) => (
<Droppable droppableId={String(droppableId)}>
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
<ListTitle
title={category.name}
/>
<ListItems category={category} show={category.items && showIndexes} />
{provided.placeholder}
</div>
)}
</Droppable>
);
List items
<Fragment>
{category.items.map((item, index) => (
<ListItem
key={index}
item={item}
index={index}
/>
))}
</Fragment>
Items
render() {
const {
item, index, categoryIndex, itemStore,
} = this.props;
return (
<Draggable key={index} draggableId={item._id} index={index}>
{(provided, snapshot) => (
<div
role="presentation"
className={cx({
'list-item-container': true,
'selected-list-item': this.isSelectedListItem(item._id),
})}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
onClick={this.handleItemClick}
>
<div className={cx('select-title')}>
<p className={cx('list-item-name')}>{item.title}</p>
</div>
{capitalize(item.importance)}
</div>
</div>
)}
</Draggable>
);
}
Method to reorder Items (I'm using Mobx-State_Tree)
reorderItem: flow(function* reorderItem(result) {
const { source, destination } = result;
const categorySnapshot = getSnapshot(self.itemCategories);
const sourceCatIndex = self.itemCategories
.findIndex(category => category._id === source.droppableId);
const destinationCatIndex = self.itemCategories
.findIndex(category => category._id === destination.droppableId);
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
if (sourceCatIndex === destinationCatIndex) {
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
try {
yield itemService.bulkEditPriorities(prioritizedItems);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
} else {
const destinationCatItems = Array.from(categorySnapshot[destinationCatIndex].items);
destinationCatItems.splice(destination.index, 0, draggedItem);
const prioritizedSourceItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedSourceItems);
const prioritizedDestItems = setItemPriorities(destinationCatItems);
applySnapshot(self.itemCategories[destinationCatIndex].items, prioritizedDestItems);
try {
const sourceCatId = categorySnapshot[sourceCatIndex]._id;
const originalItemId = categorySnapshot[sourceCatIndex].items[source.index]._id;
yield itemService.moveItemToNewCategory(originalItemId, sourceCatId, destinationCatIndex);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
}
}),
Sample data
const itemData = [
{
_id: 'category-1',
title: 'Backlog',
items: [
{ _id: 'item-1', title: 'Here and back again' },
},
{
_id: 'category-2',
title: 'In progress',
items: []
},
{
_id: 'category-3',
title: 'Done',
items: []
}
}
}
Summary
When and item is dragged and dropped, I check to see if the item is dropped in the outside the dnd context or in the same position it was dragged from. If true, i do nothing.
If the item is dropped within the context, i check to see if it was dropped in the same category. if true, i remove the item from its current position, put it in the target position, update my state, and make an API call.
If it was dropped in a different category, i remove the item from the source category, add to the new category, update the state and make an API call.
Am I missing something?
I am using both mst and the react-beautiful-dnd library
I will just paste my onDragEnd action method
onDragEnd(result: DropResult) {
const { source, destination } = result;
// dropped outside the list
if (!destination) {
return;
}
if (source.droppableId === destination.droppableId) {
(self as any).reorder(source.index, destination.index);
}
},
reorder(source: number, destination: number) {
const tempLayout = [...self.layout];
const toMove = tempLayout.splice(source, 1);
const item = toMove.pop();
tempLayout.splice(destination + lockedCount, 0, item);
self.layout = cast(tempLayout);
},
I think in order to avoid the flicker you need to avoid using applySnapshot
You can replace this logic
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
just splice the items tree
const [draggedItem] = categorySnapshot[sourceCatIndex].items.splice(destination.index, 0, draggedItem)
this way you don't need to applySnapshot on the source items after
I believe this issue is caused by multiple dispatches happening at the same time.
There're couple of things going on at the same time. The big category of stuff is going on is the events related to onDragStart, onDragEnd and onDrop. Because that's where an indicator has to show to the user they are dragging and which item they are dragging from and to.
So especially you need to put a timeout to onDragStart.
const invoke = (fn: any) => { setTimeout(fn, 0) }
Because Chrome and other browser will cancel the action if you don't do that. However that is also the key to prevent flickery.
const DndItem = memo(({ children, index, onItemDrop }: DndItemProps) => {
const [dragging, setDragging] = useState(false)
const [dropping, setDropping] = useState(false)
const dragRef = useRef(null)
const lastEnteredEl = useRef(null)
const onDragStart = useCallback((e: DragEvent) => {
const el: HTMLElement = dragRef.current
if (!el || (
document.elementFromPoint(e.clientX, e.clientY) !== el
)) {
e.preventDefault()
return
}
e.dataTransfer.setData("index", `${index}`)
invoke(() => { setDragging(true) })
}, [setDragging])
const onDragEnd = useCallback(() => {
invoke(() => { setDragging(false) })
}, [setDragging])
const onDrop = useCallback((e: any) => {
invoke(() => { setDropping(false) })
const from = parseInt(e.dataTransfer.getData("index"))
onItemDrop && onItemDrop(from, index)
}, [setDropping, onItemDrop])
const onDragEnter = useCallback((e: DragEvent) => {
lastEnteredEl.current = e.target
e.preventDefault()
e.stopPropagation()
setDropping(true)
}, [setDropping])
const onDragLeave = useCallback((e: DragEvent) => {
if (lastEnteredEl.current !== e.target) {
return
}
e.preventDefault()
e.stopPropagation()
setDropping(false)
}, [setDropping])
return (
<DndItemStyle
draggable="true"
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDrop={onDrop}
onDragOver={onDragOver}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
dragging={dragging}
dropping={dropping}
>
{(index < 100) && (
cloneElement(children as ReactElement<any>, { dragRef })
)}
</DndItemStyle>
)
})
I have to apply two more timeout invoke in the above DndItem, the reason for that is during the drop, there're two many events are competing with each other, to name a few
onDragEnd, to sugar code the indicator
onDrop, to re-order
I need to make sure re-order happens very quickly. Because otherwise you get double render, one with the previous data, and one with the next data. And that's why the flickery is about.
In short, React + Dnd needs to apply setTimeout so that the order of the paint can be adjusted to get the best result.

Nested Navigation using Redux

I have several a stackNavigator nested in a tabNavigator and this tabNavigator is nested in a stackNavigator. I have integrated redux in order to navigate easily.
My navigators :
const StackA = StackNavigator({
Main: { screen: ScreenA }
})
const TabBarNavigator = TabNavigator({
Home: { screen: StackA },
Profil: { screen: ProfilScreen }
})
const AppNavigator = StackNavigator({
Tabbar: { screen: TabBarNavigator },
Other: { screen: OtherScreen }
})
const AppWithNavigationState = ({ dispatch, nav }) => (
<AppNavigator navigation={addNavigationHelpers({ dispatch, state: nav
})} />
);
AppWithNavigationState.propTypes = {
dispatch: PropTypes.func.isRequired,
nav: PropTypes.object.isRequired,
};
const mapStateToProps = state => ({
nav: state.appNav,
});
export default connect(mapStateToProps)(AppWithNavigationState);
When I set the initial state in redux i have an error :
There is no route defined for key Tabbar. Must be one of :'StackA'
My nav reducer :
const initialNavState =
AppNavigator.router.getStateForAction(NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({
routeName: 'Tabbar',
})
],
}));
function nav(state = initialNavState, action) {
let nextState;
switch (action.type) {
case actions.types.navigate:
const { routeName } = action;
nextState = AppNavigator.router.getStateForAction(
NavigationActions.navigate({ routeName }),
state
);
break;
case 'Logout':
nextState = AppNavigator.router.getStateForAction(
NavigationActions.navigate({ routeName: 'Login' }),
state
);
break;
default:
nextState = AppNavigator.router.getStateForAction(action, state);
break;
}
return nextState || state;
}
I understand in my StackA i don't have Tabbar route but i do in my appNavigator. I want to initialise my default route to Tabbar in my AppNavigator without affected my StackA. It's possible ? if yes, can i use this navigation for my StackA ?

Resources