I have simple React app in Typescript and I am trying to utilize useMemo(), but is not working.
useMemo, if I understand correctly, should not invoke expensive operation if the input argument is equal to already calculated argument.
const memoizedResult = useMemo(() => multiply(data), [data]);
But in my case is expensive operation always called and result is not retrieved from cache.
Input value is the same 444, result is the same 888. Operation is called 20 times. Should be 1.
Check results in console:
Demo:
import React, { FC, useMemo } from "react";
function App() {
return <table><tbody>
{[...Array(20).keys()].map(it => {
return <CachedRow key={it} data={444}></CachedRow>
})}
</tbody></table>
}
export default App;
const CachedRow: FC<{ data: number; }> = ({ data = 1 }) => {
const memoizedResult = useMemo(() => multiply(data), [data]);
return (<tr>
<td>{new Date().toLocaleTimeString()}</td>
<td>-</td>
<td>{memoizedResult}</td>
</tr>);
};
function multiply(a: number): string {
console.log('expensive multiply', a);
return (a * 2).toString();
}
Related
Why I see always "loading..."?
I used redux-toolkit and createSlice and fetch data by axios.
I have not any problem by fetching data and my data is in State.
My problem is displaying fetched data.
My Component code is:
import React, {useEffect, useState} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {fetchTrackers} from 'dashboard/dashboardSlice';
export default function TrackerManagerDashboard() {
const [trackersList, setTrackersList] = useState(useSelector(state => state.trackersData));
const [activeTracker, setActiveTracker] = useState(useSelector(state => state.activeTracker));
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchTrackers);
}, []);
if(!trackersList)
return (
<div>loading...</div>
)
return (
<div className="TrackerManagerDashboard">
...
</div>
)
}
and reducer Slice file is:
import { createSlice } from '#reduxjs/toolkit'
import * as env from "../../environments";
import axios from 'axios';
const initialState = {
trackersData: {},
activeTracker: {},
}
const dashboardSlice = createSlice({
name: 'dashboard',
initialState,
reducers: {
setInitialState(state, action) {
state.trackersData = action.payload.data;
state.activeTracker = state.trackersData[Object.keys(state.trackersData)[0]];
},
},
})
export async function fetchTrackers (dispatch, getState) {
try {
const { data } = await axios.get(env.APP_URL + '/fetch/trackers.json');
dispatch(setInitialState({type: 'setInitialState', data }));
} catch(error) {
console.log(error);
}
}
export const { setInitialState} = dashboardSlice.actions
export default dashboardSlice.reducer
Never use useState(useSelector.
That means "create a component-local state with the initial value that the return value of useSelector at the time of first render has". If the Redux state changes later, you will never get to see it, as your useState is already initialized and any change there will not be reflected in your trackersList variable.
Instead, just call useSelector:
const trackersList = useSelector(state => state.trackersData);
const setActiveTracker = useSelector(state => state.activeTracker);
I am trying to create a search functionality.
So the values from the search input is actually getting passed in my actions and I can see the values from redux logger. However redux saga seems not able to intercept the payload value from the action creator. When I console log it it prints undefined.
Actions
//ACTIONS
import SearchActionTypes from "./search.types";
export const SearchActionStart = (value) => ({
type: SearchActionTypes.SEARCH_START,
value
});
export const SearchActionSuccess = (items) => ({
type: SearchActionTypes.SEARCH_SUCCESS,
payload: items,
});
export const SearchActionFailure = (e) => ({
type: SearchActionTypes.SEARCH_FAILURE,
payload: e,
});
Search Component
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectFieldData } from "../../redux/search/search.selector";
import { SearchActionStart } from "../../redux/search/search.actions";
const SearchComponent = (props) => {
const { searchResults, value } = props;
useEffect(() => {}, []);
const onSearchChange = (event) => {
const { value } = event.target;
searchResults(value);
};
return (
<div>
<input
type="text"
value={value}
onChange={onSearchChange}
/>
</div>
);
};
const mapDispatchToProps = (dispatch) => ({
searchResults: (value) =>
dispatch(SearchActionStart(value)),
});
const mapStateToProps = createStructuredSelector({
searchItem: selectFieldData,
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(SearchComponent);
searchSaga
import {
put,
call,
takeLatest,
all,
} from "redux-saga/effects";
import { SearchImage } from "../../api/search-image";
import Axios from "axios";
import {
SearchActionStart,
SearchActionSuccess,
SearchActionFailure,
} from "./search.actions";
import SearchActionTypes from "./search.types";
function* fetchFieldAsync(value) {
try {
// const images = yield call(SearchImage, value);
console.log(value);
// yield put(SearchActionSuccess(value));
} catch (e) {
yield put(SearchActionFailure(e));
console.log(e);
}
}
export function* fetchFieldStart() {
yield takeLatest(
SearchActionTypes.SEARCH_START,
fetchFieldAsync
);
}
export function* searchFieldSaga() {
yield all([call(fetchFieldAsync)]);
}
rootSaga
import { call, all } from "redux-saga/effects";
import { searchFieldSaga } from "./search/search.saga";
export default function* rootSaga() {
yield all([call(searchFieldSaga)]);
}
Please have a look into this code sandbox(https://codesandbox.io/s/basic-redux-saga-49xyd?file=/index.js) ... Your code is working fine. In saga function you will get the object that has been sent from the action as the param. You can destructure it into {value} to get the search term alone as param instead of action object.
A very silly mistake.
In my searchSaga instead of exporting the watcher function fetchFieldStart function. I mistakenly exported the intermediary functions instead, which is the fetchFieldAsync function whose job is to fetch an API.
So in
searchSaga.js
instead of:
export function* searchFieldSaga() {
yield all([call(fetchFieldAsync)]);
}
It should be:
export function* searchFieldSaga() {
yield all([call(fetchFieldStart)]);
}
For anyone who might encounter undefined error in your sagas, it might be worth reviewing if your exporting correct functions.
I hope this could also help anyone who have encountered similar problem
Thanks evryone.
I'm new to React and Redux. I've been trying to create a counter to learn how to work with Redux, but I cannot get the view to update, or mapStateToProps to be called. I've gone over the Redux troubleshooting page, and looked at a number of similar threads posted here. Could somebody please help me identify what is wrong with this code?
import React from 'react'
import { render } from 'react-dom'
import { Provider, connect } from 'react-redux'
import { createStore } from 'redux'
import './index.scss';
let Counter = props => {
console.log('>>> counter', props)
return (
<div>
<h1>counter (Redux Version)</h1>
<p>count: {props.count}</p>
</div>
);
}
let increment = () => {
return {
type: 'INC'
}
}
const counterReducer = (state = {count: 0}, action) => {
switch (action.type) {
case 'INC':
console.log('>>> inc');
return { count: state.count + 1 };
default:
return state;
}
}
const mapStateToProps = state => {
console.log('>>> map state to props', state);
return { count: state.counterReducer.count };
};
const store = createStore(counterReducer);
connect(mapStateToProps, null)(Counter);
render(
<Provider store={store}>
<Counter count={0}/>
</Provider>,
document.getElementById('root')
);
window.store = store;
window.increment = increment;
Using the Chrome console, I am able to see the sate of the store, and then dispatch the increment event. When I do this, I see that the state in the store has updated correctly but the value shown in the paragraph tag still says 0.
store.getState()
{count: 0}
store.dispatch(increment());
VM1382:1 >>> inc
{type: "INC"}
store.getState()
{count: 1}
Counter isn't updating on dispatch because Counter in render isn't actually a connected component.
According to the docs on connect (https://react-redux.js.org/api/connect):
It does not modify the component class passed to it; instead, it returns a new, connected component class that wraps the component you passed in.
This means that simply calling connect(mapStateToProps, null)(Counter); will not mutate Counter, but it'll instead return a connected version of it instead. This returned component is what you want.
A quick fix to this would be to save connect's return value:
const ConnectedCounter = connect(mapStateToProps)(Counter);
and use ConnectedCounter instead of Counter in render:
<Provider store={store}>
<ConnectedCounter count={0}/>
</Provider>
When connecting components to the store, I suggest implementing the component in its own jsx file and export the connected component as default.
In Counter.jsx, we can have...
import { connect } from 'react-redux';
const Counter = props => {
console.log('>>> counter', props)
return (
<div>
<h1>counter (Redux Version)</h1>
<p>count: {props.count}</p>
</div>
);
}
const mapStateToProps = state => {
console.log('>>> map state to props', state);
return { count: state.counterReducer.count };
};
export default connect(mapStateToProps)(Counter);
...so whenever Counter is imported somewhere else in the project (import Counter from '<path>/Counter'), we're sure that Counter is already a connected component.
That's the component in question. Before the component is mounted, it successfully dispatches an action {this.props.populateGrid()}. Everything is fine, I can see the state in the logger (basically it's a nested array of random numbers). When I press the button, it should rehydrate the state with new random numbers. Yet, I get the following error: Cannot read property 'populateGrid' of undefined.
import React, { Component, PropTypes } from 'react';
import { View, StyleSheet, Button } from 'react-native';
import Grid from './Grid';
import * as globalStyles from '../styles/global';
export default class Body extends Component {
componentWillMount() {
this.refresh();
}
refresh() {
this.props.populateGrid();
}
render() {
return (
<View style={styles.body}>
<Grid inGrid={this.props.grid} />
<Button
onPress={this.refresh}
title={'Regenerate the Grid'}
/>
</View>
);
}
}
Container:
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { listNumbers, pickNumber } from '../actions/numberActions';
import { populateRow, populateGrid } from '../actions/gridActions';
import Body from '../components/Body';
const mapStateToProps = state => ({
numbers: state.numbers,
grid: state.grid
});
const mapDispatchToProps = dispatch => (
bindActionCreators({
listNumbers,
pickNumber,
populateRow,
populateGrid
}, dispatch)
);
export default connect(
mapStateToProps,
mapDispatchToProps
)(Body);
Action:
import { POPULATE_ROW, POPULATE_GRID } from './actionTypes';
import { randNumbers, randGrid } from '../utils/generators';
export const populateRow = (n) => {
return {
type: POPULATE_ROW,
payload: randNumbers(n)
};
};
export const populateGrid = () => {
return {
type: POPULATE_GRID,
payload: randGrid()
};
};
reducer:
import { POPULATE_ROW, POPULATE_GRID } from '../actions/actionTypes';
export default (state = [], action = {}) => {
switch (action.type) {
case POPULATE_ROW:
return action.payload || [];
case POPULATE_GRID:
return action.payload || [];
default:
return state;
}
};
Generators of numbers (it's the second function in this case)
export const randNumbers = (n) => {
let numbers = new Array(n);
const shuffled = [];
// fill one array with the numbers 1-10
numbers = numbers.fill(1).map((_, i) => i + 1);
// shuffle by taking a random element from one array
// and pushing it to the other array
while (numbers.length) {
const idx = numbers.length * Math.random() | 0; // floor trick
shuffled.push(numbers[idx]);
numbers.splice(idx, 1);
}
return shuffled;
};
export const randGrid = () => {
const shuffled = randNumbers(6);
const array = shuffled.map(a => {
let r = new Array(6);
r = [a, ...randNumbers(5)];
return r;
});
return array;
};
I think you need to bind this to your refresh method in your onClick handler, so that this is set properly when refresh executes:
<Button
onPress={this.refresh.bind(this)}
title={'Regenerate the Grid'}
/>
Hope that helps!
For all I know, I have to write request in action create. How to use a promise in action for submitting a request? I am getting data in action. Then new state is created in reducer. Bind action and reducer in connect. But I don't know how to use promise for request.
Action
import $ from 'jquery';
export const GET_BOOK = 'GET_BOOK';
export default function getBook() {
return {
type: GET_BOOK,
data: $.ajax({
method: "GET",
url: "/api/data",
dataType: "json"
}).success(function(data){
return data;
})
};
}
Reducer
import {GET_BOOK} from '../actions/books';
const booksReducer = (state = initialState, action) => {
switch (action.type) {
case GET_BOOK:
return state;
default:
return state;
}
};
export default booksReducer;
Container
How display data in container?
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import getBook from '../actions/books';
import Radium from 'radium';
import {Link} from 'react-router';
function mapStateToProps(state) {
return {
books: state.data.books,
};
}
function mapDispatchToProps(dispatch) {
return {
getBooks: () => dispatch(getBook()),
};
}
#Radium
#connect(mapStateToProps, mapDispatchToProps)
class booksPage extends Component {
static propTypes = {
getBooks: PropTypes.func.isRequired,
books: PropTypes.array.isRequired,
};
render() {
const {books} = this.props;
return (
<div>
<Link to={`/authors`}><MUIButton style="flat">All Authors</MUIButton></Link>
<ul>
{books.map((book, index) =>
<li key={index}>
<Link to={`/book/${book.name}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
"{book.name}"</div></MUIButton></Link>
<Link to={`/author/${book.author}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
{book.author}</div></MUIButton></Link>
</li>
)}
</ul>
</div>
);
}
}
export default booksPage;
Since you are already using redux you can apply redux-thunk middleware which allows you to define async actions.
Installation & usage: Redux-thunk
export function fetchBook(id) {
return dispatch => {
dispatch(setLoadingBookState()); // Show a loading spinner
fetch(`/book/${id}`, (response) => {
dispatch(doneFetchingBook()); // Hide loading spinner
if(response.status == 200){
dispatch(setBook(response.json)); // Use a normal function to set the received state
}else {
dispatch(someError)
}
})
}
}
function setBook(data) {
return { type: 'SET_BOOK', data: data };
}
You should use Async Actions described in Redux Documentation
Here an example of reducer for async action.
const booksReducer = (state = {}, action) => {
switch (action.type) {
case 'RESOLVED_GET_BOOK':
return action.data;
default:
return state;
}
};
export default booksReducer;
and then you create your Async Action.
export const getBook() {
return fetch('/api/data')
.then(response => response.json())
.then(json => dispatch(resolvedGetBook(json)))
}
export const resolvedGetBook(data) {
return {
type: 'RESOLVED_GET_BOOK',
data: data
}
}
Several Notes:
We could return Promise (instead of Object) in action by using redux-thunk middleware.
Don't use jQuery ajax library. Use other library specifically for doing that (e.g. fetch()). I use axios http client.
Remember, in redux you only use pure function in reducer. Don't make ajax call inside reducer.
Read the complete guide from redux docs.
You should be able to use dispatch inside the callback (if you pass it as an argument):
export default function getBook(dispatch) {
$.ajax({
method: "GET",
url: "/api/data",
dataType: "json"
}).success(function(data){
return dispatch({type:'GET_BOOK', data: data});
});
}
Then, pass dispatch to the action:
function mapDispatchToProps(dispatch) {
return {
getBooks: () => getBook(dispatch),
};
}
Now, you should have access to the action.data property in the reducer:
const booksReducer = (state = initialState, action) => {
switch (action.type) {
case GET_BOOK:
//action.data <--- here
return state;
default:
return state;
}
};
You might want to separate concerns, to keep action creators "pure".
Solution; write some middleware. Take this for example (using superagent).
import Request from 'superagent';
const successHandler = (store,action,data) => {
const options = action.agent;
const dispatchObject = {};
dispatchObject.type = action.type + '_SUCCESS';
dispatchObject[options.resourceName || 'data'] = data;
store.dispatch(dispatchObject);
};
const errorHandler = (store,action,err) => {
store.dispatch({
type: action.type + '_ERROR',
error: err
});
};
const request = (store,action) => {
const options = action.agent;
const { user } = store.getState().auth;
let method = Request[options.method];
method = method.call(undefined, options.url)
if (user && user.get('token')) {
// This example uses jwt token
method = method.set('Authorization', 'Bearer ' + user.get('token'));
}
method.send(options.params)
.end( (err,response) => {
if (err) {
return errorHandler(store,action,err);
}
successHandler(store,action,response.body);
});
};
export const reduxAgentMiddleware = store => next => action => {
const { agent } = action;
if (agent) {
request(store, action);
}
return next(action);
};
Put all this in a module.
Now, you might have an action creator called 'auth':
export const auth = (username,password) => {
return {
type: 'AUTHENTICATE',
agent: {
url: '/auth',
method: 'post',
resourceName: 'user',
params: {
username,
password
}
}
};
};
The property 'agent' will be picked up by the middleware, which sends the constructed request over the network, then dispatches the incoming result to your store.
Your reducer handles all this, after you define the hooks:
import { Record } from 'immutable';
const initialState = Record({
user: null,
error: null
})();
export default function auth(state = initialState, action) {
switch (action.type) {
case 'AUTHENTICATE':
return state;
case 'AUTHENTICATE_SUCCESS':
return state.merge({ user: action.user, error: null });
case 'AUTHENTICATE_ERROR':
return state.merge({ user: null, error: action.error });
default:
return state;
}
};
Now inject all this into your view logic. I'm using react as an example.
import React from 'react';
import ReactDOM from 'react-dom';
/* Redux + React utils */
import { createStore, applyMiddleware, bindActionCreators } from 'redux';
import { Provider, connect } from 'react-redux';
// thunk is needed for returning functions instead
// of plain objects in your actions.
import thunkMiddleware from 'redux-thunk';
// the logger middleware is useful for inspecting data flow
import createLogger from 'redux-logger';
// Here, your new vital middleware is imported
import { myNetMiddleware } from '<your written middleware>';
/* vanilla index component */
import _Index from './components';
/* Redux reducers */
import reducers from './reducers';
/* Redux actions*/
import actionCreators from './actions/auth';
/* create store */
const store = createStore(
reducers,
applyMiddleware(
thunkMiddleware,
myNetMiddleware
)
);
/* Taint that component with store and actions */
/* If all goes well props should have 'auth', after we are done */
const Index = connect( (state) => {
const { auth } = state;
return {
auth
};
}, (dispatch) => {
return bindActionCreators(actionCreators, dispatch);
})(_Index);
const provider = (
<Provider store={store}>
<Index />
</Provider>
);
const entryElement = document.getElementById('app');
ReactDOM.render(provider, entryElement);
All of this implies you already set up a pipeline using webpack,rollup or something, to transpile from es2015 and react, to vanilla js.
Consider using the new thunk API
export const load = createAsyncThunk(
'example/api',
async (arg, thunkApi) => {
const response = await fetch('http://example.api.com/api')
if (response.status === 200) {
const json = await response.json()
return json
},
)
Also, in the new redux template application, actions are part of the reducer/slice, and you can use extraReducers to response to events related to the async action status. It is much simpler using redux this way.
See documentation of async thunk here: https://redux.js.org/usage/writing-logic-thunks