I have been handed a project and been told to use React Hooks instead of Redux as much as possible. Is it possible to replace the reducers and thunks below with React Hooks? Is it worth it to replace?
Reducers/index.js
import { combineReducers } from 'redux'
import {createActions, createReducer, Types as ReduxSauceTypes} from 'reduxsauce'
import { reducer as ProfileReducer } from '#Reducers/Profile'
const appReducer = combineReducers({
profile: ProfileReducer,
// other reducers
})
const { Types, Creators: Actions } = createActions({
resetApp: []
})
const rootReducer = createReducer([], {
[Types.RESET_APP]: (state, action) => {
return appReducer(undefined, action)
//Passing undefined as state will make all the reducers using their initial states.
},
[ReduxSauceTypes.DEFAULT]: (state, action) => {
return appReducer(state, action)
}
})
const resetReduxStore = () => {
return dispatch => {
dispatch(Actions.resetApp())
}
}
export { rootReducer, resetReduxStore }
Reducers/Profile/index.js
import { createActions, createReducer } from 'reduxsauce';
export { default as thunks } from './thunks.js';
/* ------------- Initial State ------------- */
export const INITIAL_STATE = {
user: {},
};
/* ------------- Types and Action Creators ------------- */
export const { Types, Creators } = createActions({
setUser : ['user'],
});
/* ------------- Hookup Reducers To Types ------------- */
export const reducer = createReducer(INITIAL_STATE, {
[Types.SET_USER]: (state, { user }) => {
return {
...state,
user
};
},
});
export default Creators;
"Is it worth it?" is a matter of opinion, but I'll give mine.
Re: useReducer
React now has a useReducer hook that allows you to update a state by dispatching actions. You can use the same reducers that you have already. By default, useReducer only manages the state for the component that it's in. It doesn't come with a context provider. You could create your own context but at that point you are re-creating Redux.
My opinion: Use the React useReducer hook if you have state that it localized to one part of your app. Keep the reducer in some component and pass down callbacks and values via props. If you have state that is global in nature then use Redux.
Re: useState
You asked if you can replace your reducers. As I said, the useReducer hook would use the same sort of reducer so you wouldn't be replacing it. Perhaps the question you need to be asking is do I need a reducer-like system to update this state?
My Opinion: The code that you've posted here is extremely simple and it would be a good candidate for a basic useState hook.
const [user, setUser] = useState();
You could combine this with a context provider and a useContext hook to have a global profile state. If you start having multiple contexts for multiple states -- that's when you want to use Redux instead.
const UserContext = React.createContext([
undefined, // user
() => {} // setUser
]);
// takes no `value` because the state is internal
export const UserProvider = ({children}) => {
const [user, setUser] = React.useState<MaybeUser>();
return (
<UserContext.Provider value={[user, setUser]}>
{children}
</UserContext.Provider>
)
}
export const useUser = () => useContext(UserContext);
Usage in some component:
const Test = () => {
const [user, setUser] = useUser();
...
}
Re: Thunks
Thunks are function of dispatch so they can easily be rewritten with the useDispatch hook, if using Redux. You can use the useSelector hook instead of the getState() argument of a thunk.
There are lots of ways to replace thunks using hooks. But the only thunk that you have here is your resetReduxStore which really doesn't need to be a thunk.
Related
I'm new in react native world and i'm on a new project with store (manage by redux).
I encounter an issue with custom hooks and useEffect
here my custom hooks
const useTheme = () => {
const [activeTheme, setActiveTheme] = useState();
const { id: universID, defaultTheme: universDefaultTheme } = useSelector(
(state) => state.univers
);
const { theme } = useSelector((state) => state);
const { themes: activeThemes } = useSelector((state) => state.settings);
const dispatch = useDispatch();
//set theme when univers change
useEffect(() => {
console.log('TODO TOO MANY CALLS!!!!!', universID);
if (universID) {
setTheme(
activeThemes.find((theme) => theme.univers === universID)?.theme
);
}
}, [universID]);
//get active theme of current univers
useEffect(() => {
setActiveTheme(
activeThemes.find((theme) => theme.univers === universID)?.theme
);
}, [activeThemes]);
... rest of code ...
return {
theme,
activeTheme,
setTheme,
};
}
on components i use
const {
theme: { colors },
} = useTheme();
My issue is that on every import the useEffect(()=>{},[universID]) is trigger. UniversID come from redux store.
If i understand clearly when i import useTheme() the reference of universID change because there are copy of universID from store created, and reference change.
if i pass universID as arguments to useTheme hooks there are no problem, cause reference is the same. But if i do this i need tu make a useSelector(universID) on every components who import useTheme hooks.
My understanding of mecanism is good ?
There are a way to get universID from store with the same reference on every import, for not trigger useEffect(,[universID]) on every import ? without pass universID as arguments of useTheme (i.e. useRef, useCallback) ?
Thanks for the time past to read (or better, to answer ;))
I am new to React Native Programming. So, please tell me in detail. thank you.
calling use Selector
I am calling use Selector inside my functional component like this:
import { useDispatch, useSelector } from 'react-redux';
const AddAddressScreen = ({ navigation }) => {
const dispatch = useDispatch();
const data = useSelector(state => state);
console.log(data + "happy Coding");
return (
<View style={styles.container}>
<View>
);
}
export default AddAddressScreen;
My reducer looks like this
case types.API_LOGIN_SUCCESS:
if (action.result.result.mobile_verified === false) {
return {
...state,
onLoad: false,
result: action.result,
status: action.status,
error: null,
navigation: action.navigation.navigate("VerifyMNO")
};
} else {
return {
...state,
onLoad: false,
result: action.result,
status: action.status,
error: null,
navigation: action.navigation.navigate("AddAddress")
};
}
here my mobile number is verified so I move to the address screen.
where I use Use Selector which gives me an error. while I remove above two lines my code runs successfully.
My saga looks like this
export function* watchLoginUserInfo() {
yield takeLatest(types.LOGIN_USER, loginApiSaga)
}
My root saga
import { all, fork } from 'redux-saga/effects';
import { watchLoginUserInfo, } from './authenticationSagas';
function* rootSaga() {
yield all([
watchLoginUserInfo(),
])
}
export default rootSaga;
My Store looks like this
import {createStore, applyMiddleware} from 'redux';
import rootReducer from '../redux/reducers/root-reducer.js'
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../redux/sagas/rootSaga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
export {store};
when ever I use use Selector hook in my code it gives me the following error.
error 1
error 2, 3, 4
Use the select effect from redux-saga inside of a reducer: https://redux-saga.js.org/docs/api/#selectselector-args
For example const selectedState = yield select(state => state);.
The useSelector hook is for use inside of a function component.
EDIT: since the above doesn't seem to be the issue, I think the issue is that you're calling navigation functions from within your reducer. Reducer code can have no side effects, so you can't call navigation.navigate(...) from within the reducer. This will need to happen in the saga code instead. It might be able to be done in the loginApiSaga or in a dedicated saga that is triggered by API_LOGIN_SUCCESS.
I'm trying to access a redux store with redux persist in a service for my react native app.
I need a specific token to set a websocket connection.
My code so far:
./redux/Store.js:
const persistedReducer = combineReducers({
tokens: persistReducer(secureConfig, TokensReducer),
});
const store = createStore(persistedReducer);
const configureStore = () => {
const persistor = persistStore(store);
return { persistor, store };
};
export default configureStore;
./redux/reducers/TokenReducer
const initialState = {
accessToken: null,
refreshToken: null
}
const TokensReducer = (state = initialState, action) {
// reducer
};
export default TokensReducer;
./service/websocket.js
import configureStore from '../redux/Store';
const { store } = configureStore();
console.log(store.getState().tokens);
The problem is, I'm not getting the persisted content, but I'm getting the initial state (accessToken = null, refreshToken = null).
When I access the store from inside my app (inside components inside and ), I get the correct values.
Edit:
when I wrap the console.log in a setTimeout() of let's say 1 second, it works! So it asynchronous, but how can I create my code to wait for it and not using setTimeout?
I'm developing a Chrome extension using one of the Vue js boilerplates from Github. The default boilerplate setup is as follows:
store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import mutations from './mutations';
import * as actions from './actions'; // all actions are imported as separate vars
Vue.use(Vuex);
export default new Vuex.Store({
state: { },
mutations,
actions
});
Then in actions.js
import * as types from './mutation-types';
export const setFoo = ({ commit }, payload) => {
commit(types.SET_FOO, payload); // SET_FOO is defined in the mutation-types file
};
I think the above approach lacks a fundamental reason why we want to use mutation types file - to avoid retyping the names for mutations and actions.
So instead, I came up with a different approach:
store/index.js
...
import actions from './actions'; // actions are imported as separate functions
...
Then in actions.js
import * as types from './mutation-types';
export default {
[types.UPDATE_FOO] ({commit}, payload) {
commit(types.UPDATE_FOO, payload);
}
}
Then anywhere in the extension, we could also import mutation-types and dispatch actions using const names like so:
store.dispatch(types.UPDATE_FOO, 'some value');
The second approach seems to be more practical in terms of naming and then dispatching/committing our actions/mutations. Or could there be any issues with the latest?
Which of the above, would be generally better practice?
The first approach is preferable, but it's completely up to you. Similar approach is used in official Vuex docs.
Refrence
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// we can use the ES2015 computed property name feature
// to use a constant as the function name
[SOME_MUTATION] (state) {
// mutate state
}
}
})
// actions.js
actions: {
checkout ({ commit, state }, products) {
// save the items currently in the cart
const savedCartItems = [...state.cart.added]
// send out checkout request, and optimistically
// clear the cart
commit(types.CHECKOUT_REQUEST)
// the shop API accepts a success callback and a failure callback
shop.buyProducts(
products,
// handle success
() => commit(types.CHECKOUT_SUCCESS),
// handle failure
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
I have a custom reducer and a connected component to change its state. Now I'd like to use this state as a permanent filter on List elements.
I understand the List elements are connected to the redux-state, so I hope I'm able to access it through the List component's props, but couldn't find a way how to do that.
The List component is connected but not yours.
import { connect } from "react-redux";
const MyList = ({ is_published, ...props }) => (
<List {...props} filter={{ is_published }}>
</List>
);
const mapStateToProps = state => ({
is_published: state.myCustomReducer.is_published,
});
export default connect(mapStateToProps, undefined)(MyList);
Edit:
Just found out we don't update data when this prop change. This is a bug and you can open an issue about it.
In the mean time, here's a workaround:
Create a custom saga listening to whatever action you use alongside your custom reducer (I'll call it SET_IS_PUBLISHED for my example). This custom saga should put the changeListParams action creator from react-admin with your filter.
It will probably looks like this (not tested):
import { takeEvery, put, select } from 'redux-saga/effects'
import { changeListParams } from 'react-admin'
import { SET_IS_PUBLISHED } from './isPublished'
const getCurrentListParams = (state, resource) => {
const resourceState = state.admin.resources[resource]
return resourceState.list.params
}
function handleSetPublished({ payload }) {
const currentParams = yield select(getCurrentListParams)
const newParams = {
// Keep the current params
...currentParams,
// Override the filter
filter: {
// Keep the current filter
...currentParams.filter,
// Only override the is_published
is_published: payload
}
}
// Dispatch the action for the `posts` resource
yield put(changeListParams('posts', newParams))
}
export default function* () {
yield takeEvery(SET_IS_PUBLISHED, handleSetPublished)
}
just to bring this into 2021, you can use the useSelector redux hook to get hold of your custom state:
import { useSelector } from 'react-redux';
const MyCustomThing = (props) => {
const is_published = useSelector(state => state.customState.is_published);
}
For completeness, react-admin provides a customReducers prop to its <Admin> component so you can extend the redux state with your custom values:
const customStateReducer = (customState = { is_published: false }, { type, payload }) => {
if (type === 'IS_PUBLISHED') customState.is_published = payload.is_published;
return customState;
}
<Admin customReducers={{ customState: customStateReducer }} ...>
etc