I am new to react and redux ... so please forgive me for noobie mistakes. I read several documentations of redux and came to conclusion that this is how i should store state of react component. I require redux because there are many more nested components which need quick access of data. however... when I try to export store ... I can't find how to do so.
My App.js
export default class App extends Component {
state = {
xx: null,
yy: null
}
componentDidMount(){
//some logic
// State gets data from api
this.setState({
xx: someval,
yy: someval2
});
}
render() {
const obj = {
xx: this.state.xx,
yy: this.state.yy
};
userReducer(obj,updateUserDetails());
const store = createStore(userReducer);
return (
<Provider store={store} >
<UserDetails props ={this.state} />
</Provider>
);
}
}
// Reducer function
export const userReducer = (state, action) => {
console.log("in reducer " + JSON.stringify(state));
switch(action.type) {
case 'UPDATE_USER_INFO':
state = {
...state,
xx: action.payload.xx,
yy: action.payload.yy
}
break;
}
return state;
}
// Doesn't work
export const store = createStore(userReducer)
// Action
export const updateUserDetails = () => {
return {
type: 'UPDATE_USER_INFO'
}
}
I can't figure out way to export store so that it is accessible to nested components. Kindly help
Thanks in advance!
From looking on your code, I can see a few issues that each one can be your problem.
while reducer first loads, it has to hold initial value for the state to begin with, not related to the one you want to be set at component mount
// assiging empty obj as initial value
export const userReducer = (state = {}, action)
Actions of redux are higher order functions, returning object to be dispatch
export const updateUserDetails = () => (dispatch, getState) => {
return dispatch ({
type: 'UPDATE_USER_INFO'
})
}
About your createStore, declare here as well initial value
// assign empty obj as initial value
export const store = createStore(userReducer, {})
hope it is helpful, anyhow I recommended on looking through the docs again
Related
I'm trying to learn Redux.
I have this action which fetches data:
export const FETCH_SUCCESS = "FETCH_SUCCESS";
import axios from "axios";
export const fetchData = () => {
return async dispatch => {
try {
const result = await axios(
`url`
);
dispatch({ type: FETCH_SUCCESS, payload: { result } });
} catch (error) {
console.log(error);
}
};
};
And this reducer:
import { FETCH_SUCCESS } from "../actions/mainCategories";
const initialState = {
mainCategories: []
};
const mainCategoriesReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_SUCCESS:
return { data: action.payload.result.data };
default:
return state;
}
};
export default mainCategoriesReducer;
I use this twice, one for the mainCategories and the exact same code for my subcategories but in different files, so that I can combine it in App.js like this:
import mainCategoriesReducer from "./store/reducers/mainCategories";
import subCategoriesReducer from "./store/reducers/subCategories";
const rootReducer = combineReducers({
mainCategories: mainCategoriesReducer,
subCategories: subCategoriesReducer,
});
const store = createStore(rootReducer, applyMiddleware(ReduxThunk));
I wrap the Provider with the stored store around my App:
<Provider store={store}>
<App />
</Provider>
In the components I use it like this:
import { fetchData } from "../store/actions/mainCategories";
const mainCategories = useSelector(state => state.mainCategories);
useEffect(() => {
dispatch(fetchData());
}, [dispatch]);
And the same for the subCategories but with the corresponding imports and states (state.subCategories)
Everything is working as expected. When the App loads, it fetches my mainCategories. I can navigate to my subcategories but when I go back, the mainCategories are overwritten by the subCategories.
It seems like the combineReducers merges / overwrites instead of creating the different states. What am I doing wrong? Thank you
You need different type names to make this work. When you combine reducers, it's important that the app knows how to route your actions to the correct store. If you want to call them both FETCH_SUCCESS, I think that's fine, but add the path in front of the definition:
in your main categories actions file:
export const FETCH_SUCCESS = "mainCategories/FETCH_SUCCESS";
and in your subcategories actions file:
export const FETCH_SUCCESS = "subCategories/FETCH_SUCCESS";
That should work, but you may also need to rename the FETCH_SUCCESS variables if it's still not working.
We have set up a project with redux. In this project, we get an info objecat from an api and insert it into the store. Now we noticed that the function components re-render even if the api return the same state as in the previous request.
We think it's because we are overwriting the store but we are not sure.
ChatContainer.js
const mapStateToProps = function (state) {
return {
content: state.info.content,
loading: state.info.loading,
}
}
const ChatContainer = connect(
mapStateToProps,
)(Chat)
export default ChatContainer
Chat.js
function Chat(props) {
const { content, loading } = props;
return (
<Info content={content} loading={loading} />
)
}
action.js
export function setInfo(info) {
return {
type: SET_INFO, info: {
content: info,
loading: false
}
}
}
reducer.js
function setInfo(state = { content: [], loading: true }, action) {
switch (action.type) {
case SET_INFO:
return action.info
default:
return state
}
}
const appReducer = combineReducers({
...
info: setInfo,
...
})
export default appReducer
If state.info.content is an object, every time you change it with setInfo it will have a new reference. React-redux does a shallow compare on the result of mapStateToProps, so if your content is a different reference every time your component will re-render. connect HOC has an options parameter that you can use to implement a custom compare.
My advice would be to add a check to your setInfo or to the code calling setInfo and not calling your API if data is already loaded/didn't change(don't know your business logic).
Solution(updated):
I thought any action would cause react-redux-connect to call the mapState functions but when an action doesn't change anything then this is not the case.
I have a localStorage module that dispatches actions but don't change state, instead thy will write to localStorage. The module has selectors that are used in the containers but they won't get called until the state actually changes so the UI would only show correctly after another action was dispatched that would change the state.
Problem
When I put the store on window (window.store=store), add a console.log in the mapStateToProps, then in the console I dispatch an action: store.dispatch({type:'some action'}) then the console.log of the mapStateToProps does not show.
I do memoize the result but the mapStateToProps should be called see here
Full code is here and running example here (you can open a console clicking on 'console' link in the right bottom of the screen).
package.json
store.js:
import { createStore } from 'redux';
export default (initialState, reducer) => {
const store = createStore(
reducer,
initialState,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
window.store = store;
return store;
};
app.js
import React from 'react';
import { connect } from 'react-redux';
import './App.css';
import createStore from './store';
import { Provider } from 'react-redux';
import initCounter from './components/Counter';
import {
createWrapper,
memoize,
} from './components/#common';
const COUNTER = 'COUNTER';
const selectCounterState = state => state.myCounter;
const counter = initCounter({
actionWrapper: createWrapper(COUNTER, 'counter1'),
selectors: { myState: selectCounterState },
connect,
memoize,
});
const initialState = {
myCounter: counter.initialState,
};
const reducer = (state = initialState, action) => {
if (action.emittedBy === COUNTER) {
return {
...state,
myCounter: counter.reducer(
selectCounterState(state),
action.payload
),
};
}
return state;
};
const store = createStore(initialState, reducer);
const Counter = counter.container;
const App = () => (
<Provider store={store}>
<Counter id="counter1" parentId={[]} />
</Provider>
);
export default App;
component/Counter/index:
import component from './component';
const INCREASE = 'INCREASE';
const reducer = (state, action) => {
if (action.type === INCREASE) {
return { ...state, count: state.count + 1 };
}
return state;
};
const makeState = memoize =>
memoize((id, parentId, { count }) => ({
id: parentId.concat(id),
parentId,
count,
}));
const mapStateToProps = ({ myState }, memoize) => () => {
const newState = makeState(memoize);
return (state, ownProps) =>
console.log('in map state to props', new Date()) ||
newState(
ownProps.id,
ownProps.parentId,
myState(state)
);
};
export default ({
actionWrapper,
selectors,
connect,
memoize,
}) => {
const actions = {
increase: ({ id }) =>
actionWrapper({
type: INCREASE,
id,
}),
};
const container = connect(
mapStateToProps(selectors, memoize),
actions
)(component);
return {
container,
reducer,
initialState: { count: 0 },
};
};
components/counter/component.js:
import React from 'react';
export default props => (
<div>
<button onClick={() => props.increase(props)}>
add
</button>
{props.count}
</div>
);
This problem was caused because I had a localStorage module that did dispatch actions but did not change the state, instead it would write to localStorage.
The module had selectors that would get the right data and the containers would use them to build the correct state but since the dispatched action did not change the state in the redux store react-redux would skip calling my mapState functions (probably memoizing state in Provider).
The solution is to let the root reducer return a new state reference {...state} so any action would cause the mapState functions to be called.
Your example codepen works just fine, you just have to trigger an action that gets past your top level guard and is of the expected structure, as to not cause any followup errors:
Post this into the console of your codepen:
store.dispatch({emittedBy: "COUNTER", type: "COUNTER -> INCREASE", id: "counter1", payload: {type: "INCREASE", id: ["counter1"]}})
I'm trying to learn/ get my head around immutable so I can set it up with React/ Redux. Here's my store setup:
store.js
const store = createStore(
rootReducer,
applyMiddleware(thunk)
)
combine reducers:
import { combineReducers } from 'redux';
import { UIreducer } from './UI-reducer';
export default combineReducers({
UIreducer
});
example reducer:
import { fromJS } from 'immutable';
const initialState = fromJS({
test: false
});
export const UIreducer = (state = initialState, action) => {
switch (action.type) {
case 'TEST': {
return state.set('test', true)
}
default: return state
}
}
I'm fairly sure i've set the above two parts up correctly, the only thing that i'm not sure about is how to map the state to props in my component:
Here's how I normally do it:
const mapStateToProps = state => ({
UI: state.UIreducer
})
When using immutable it returns an object that immutable generates which doesn't look like a normal state object if I were to not use immutable. I tried after a bit of research to use .get with the state like so:
const mapStateToProps = state => ({
UI: state.get('UIreducer')
})
This however returned the error message:
state.get is not a function
Could someone please point out where i've gone wrong?
Remember the state is the combination of all reducers, so the state will look like the combined reducers object
export default combineReducers({
UIreducer
});
At mapStateToProps you should ask for state.UIreducer and at the component level UI.get('test')
Answering further questions:
where exactly would I write the UI.get('test')?
Anywhere in your component UI will be a prop for it, you can use it like this
const myComponent = (props) => {
return <div>props.UI.get('test')</div>
}
I would have thought the .get('test') would go in the mapStateToProps somehwere.
That depends of which properties of your state you want to pass/map to your component, you can also pass an specifict
const mapStateToProps = state => ({
test: state.UIreducer.get('test')
})
and inside of you component
const myComponent = (props) => {
return <div>props.test</div>
}
You could use redux-immutable and convert the initialState to an immutable object:
import {
combineReducers
} from 'redux-immutable';
import {
createStore
} from 'redux';
const initialState = Immutable.Map();
const rootReducer = combineReducers({UIreducer});
I've tried all of the related questions here in Stack Overflow and still didn't find a solution to this problem.
I have a reducer called me and I'm trying to update an array of objects in it called folders, whenever I update the me reducer the component doesn't update.
Here's how I'm updating the reducer in my component:
class ComponentA extends Component {
...
updateUploadedFiles(file) {
console.log(this.props.store);
const newFolders = this.props.me.folders.map(
folder =>
folder._id === file.parent._id
? {
...folder,
files: [...folder.files, file.file]
}
: folder
);
this.props.updateMe({
...this.props.me,
folders: newFolders
});
}
...
}
function mapStateToProps(state) {
return {
me: state.me,
path: state.path,
filesToUpload: state.uploads
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
updatePath,
updateMe,
updateUploads
},
dispatch
);
}
export default connect(mapStateToProps, mapDispatchToProps, null, {
pure: false
})(Upload);
this is my updateMe action's code:
export const updateMe = state => ({
type: "UPDATED_ME",
payload: state
});
And this is the me reducer's code:
export default function(state = "NOT_AUTHENTICATED", action) {
switch (action.type) {
case "UPDATED_ME":
return action.payload;
default:
return state;
}
}
Also here's how I'm combining the reducers:
import me from "./me";
...
import { combineReducers } from "redux";
const reducers = combineReducers({
me,
...
});
export default reducers;
This is not how redux works.
In order to update any part of your Redux store you must dispatch an action in order to let Redux "know" that the store changed and update any dependent component.
You state object must be immutable.