Component only rerenders if ternary expression is directly inside render() method - react-redux

I'm building an app with React, Redux and TypeScript.
In the top navbar I have a "Log in" link which when clicked dispatches an action. For now, all this action does is set a boolean called auth to true.
I have a lot of connected components which listen to that auth property of the redux store and decide which sub-components to render, based on ternary expressions that evaluate this.props.auth.
I was surprised to see that when I clicked "Log in" some components would rerender as expected while others would have their state successfully changed but would only alter their display if I refreshed the page or routed away and came back. After some hours of hair-pulling I believe I finally isolated the difference between the two kinds of components described above: if the ternary expression that evaluates this.props.auth is directly inside the render() method, the component behaves as expected, however, if the ternary expression is inside a .map() function which is then called by the render() method, then this weird behavior happens where I have to refresh in order for the correct rendering to match the prop values. What's going on? Does this lose it's value, is this a sync/async problem?
class LatestArticles extends Component<LatestArticlesProps> {
public latestArticlesList: JSX.Element[] = Articles.map((a: IArticle) => {
return (
<React.Fragment key={a.id}>
// some TSX
{this.props.auth === true ? <UserImgOverlay /> : <UnlockButton />}
// some more TSX
</React.Fragment>
)
});
public render(): JSX.Element {
return (
<React.Fragment>
// some TSX
{this.latestArticles}
// some more TSX
</React.Fragment>
)
}
Do let me know if you need any more context but I would like to ask for help understanding what's going on. Why does the prop change only trigger a rerender if the ternary expression is directly inside the render() method and is there any way to go around this while still mapping the data? Thank you for your attention.
EDIT
Here's my mapStateToProps:
// Components/Navbar/index.tsx
const mapStateToProps = ({ articles, auth }: IApplicationState) => {
return {
articlesPerPage: articles.articlesPerPage,
articlesPerPageStep: articles.articlesPerPageStep,
auth: auth.auth
}
}
I actually have another example of this kind of behavior happening with an onClick method:
class LatestArticles extends Component<LatestArticlesProps> {
public latestArticlesList: JSX.Element[] = Articles.map((a: IArticle) => {
return (
<React.Fragment key={a.id}>
// some TSX
<StarsRating rating={3} onClick={this.handleRatingClick} />
// some more TSX
</React.Fragment>
)
});
constructor(props: LatestArticlesProps & IOwnProps) {
super(props)
this.handleRatingClick = this.handleRatingClick.bind(this);
}
public render(): JSX.Element {
return (
<React.Fragment>
// some TSX
{this.latestArticles}
// some more TSX
</React.Fragment>
)
}
public handleRatingClick = () => {
alert('Clicked!')
}
}
^ When I click on the star nothing happens, but if I extract <StarsRating rating={3} onClick={this.handleRatingClick} /> from the .map function and put it directly inside the render() method, I get the alert saying 'Clicked!'... I suspect this is losing its value but I'm not sure how to test that.

I think the problem is with the latestArticlesList variable. It is initialized too early. It should be a function:
public latestArticlesList: JSX.Element[] = () => Articles.map((a: IArticle) => {
I assume Articles should be this.props.articlesPerPage.
And finally when you use it, call it:
<React.Fragment>
// some TSX
{this.latestArticles()}
// some more TSX
</React.Fragment>

Related

How to use mapStateToProp in react redux

I am creating search function, as a request I have to use mapStateToProps to get the data, how should I do in this file?
This is my code for that
import React, { useState, useEffect } from "react";
import useDebounce from "../../custom-hook/index";
import "../Search/index.css";
function Search() {
// State and setter for search term
const [searchTerm, setSearchTerm] = useState("");
// State and setter for search results
const [results, setResults] = useState([]);
// State for search status (whether there is a pending API request)
const [isSearching, setIsSearching] = useState(false);
// Run every 5ms
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// Here's where the API call happens
// We use useEffect since this is an asynchronous action
useEffect(
() => {
// Make sure we have a value (user has entered something in input)
if (debouncedSearchTerm) {
// Set isSearching state
setIsSearching(true);
// Fire off our API call
searchCharacters(debouncedSearchTerm).then(results => {
// Set back to false since request finished
setIsSearching(false);
// Set results state
setResults(results);
});
} else {
setResults([]);
}
},
// This is the useEffect input array
[debouncedSearchTerm]
);
// Pretty standard UI with search input and results
return (
<div>
<input
placeholder="Search..."
className="search"
onChange={e => setSearchTerm(e.target.value)}
/>
{isSearching && <div>Searching ...</div>}
{results.map(result => (
<div key={result.id}>
<h4>{result.title}</h4>
<img
src={`${result.thumbnail.path}/portrait_incredible.${
result.thumbnail.extension}`}
alt="something"
/>
</div>
))}
</div>
);
}
// API search function
function searchCharacters(search) {
console.log(search);
}
export default Search;
As my code above, you can see that in return I have isSearching fucntion, it set by useState, but now I don't do like that, I have to set mapSateToProp to get prop from store
Please help me for this thank you so much
This piece of code is taken from react-redux documentation. You should import connect function form this library and then wrap your component.
function mapStateToProps(state) {
const { todos } = state
return { todoList: todos.allIds }
}
export default connect(mapStateToProps)(TodoList)
Updated:
Since you are using hooks useSelector might work better for you.

Block navigation action in onNavigationStateChange in StackNavigator

In my StackNavigator I want to sometimes block a navigation action. I thought to use onNavigationStateChange however it is not working.
Simple example:
render() {
return <Stack onNavigationStateChange={this.handleNavigate} />
}
handleNavigate = (stateOld, stateNew, action) => {
return false; // block it
}
However it is not blocking it. Does anyone have any ideas?
Use onShouldStartLoadWithRequest instead
http://facebook.github.io/react-native/docs/webview.html#onshouldstartloadwithrequest

Complex navigation in React Native using react-navigation and Redux

I have the following navigation structure in my React Native app:
StackNavigator configured with 3 routes:
Splash screen (React Component)
StackNavigator for my login flow
DrawerNavigator for my core app screens.
The DrawerNavigator has some dynamic multiple routes, but also one static route which is another StackNavigator.
Everything seems to be working as expected:
The store is being updated accordingly.
Navigation between screen works.
Go back between screen works when configured within each component, with the following command:
this.props.navigation.goBack();
My question is - is there a way for me to handle back button on Android globally? Currently when I click on the back button, nothing happens (due to the fact I'm using Redux). Should I handle the back button in each component or is there a way of doing it using Redux?
A bit late, but there is a way to handle this with redux. In your index.js file where you create your store you can make export a class and add a componentWillMount call to handle dispatching a call to your redux actions. Just remember to import the actions you need above.
const store = configureStore();
export default class Index extends Component {
componentWillMount = () => {
BackHandler.addEventListener('hardwareBackPress', () => {
const { nav: { routes } } = store.getState();
const currentRouteName = routes[routes.length-1].routeName;
if (currentRouteName === 'EditCoupleProfile') {
store.dispatch(editCoupleActions.navigateBack())
} else if ( currentRouteName === 'EditInterests' ) {
store.dispatch(interestsActions.navigateBack())
} else {
store.dispatch(popFromStack());
}
return true;
})
};
componentWillUnmount = () => {
BackHandler.removeEventListener('hardwareBackPress');
};
render() {
return (
<Provider store={store}>
<AppWithNavigation />
</Provider>
);
}
}

Which component should control the loading state of a lower component?

Let's say I have these components:
Translator
TranslationList
Translator determines translation context, has translate function.
TranslationList must show these "visual states": loading, result list, no results.
The Translator moves around the page (one instance): on focusing an input, it moves "below" it and gives a dropdown with suggestion.
So each time it moves, it has to:
Show that it's loading translations
Show translation list or no results message.
So my question is:
Which component should control the "loading" visual state?
If the Translator component controls it, it has to pass loading=true translations=[] as props to Translation list. Then later it has to rerender it again with new props loading=false translations=[...]. This seems a bit counter-intuitive, because loading feels like the state of the TranslationList component.
If we the TranslationList component has loading state, then it also has to have a way to translate things, meaning that I have to pass translate function as prop. I would then hold translations and loading as state. This all gets a bit messy, since it must now also receive string to translate, context.
I also don't want to have separate components for loading message, no results message. I'd rather keep these inside the TranslationList, because these 3 share that same wrapper <div class="list-group"></div>
Perhaps there should be one more Component in between these two components, responsible only for fetching translation data?
Translator component should control the loading state of a lower component list component. hold the loading and translating logic but with help by wrapping it in a high order component where you should put most of the logic. link for HOC https://www.youtube.com/watch?v=ymJOm5jY1tQ.
const translateSelected = wrappedComponent =>
//return Translator component
class extends React.Component {
state = {translatedText: [], loading:true}
componentDidMount(){
fetch("text to translate")
.then(transText => this.setState({translatedText: transText, loading: false}))
}
render() {
const {translatedText} = this.state
return <WrappedComponent {..this.props} {...translatedText}
}
}
const Translator_HOC = translateSelected(Translator);
You could introduce a Higher Order Component to control the switching of the loading state and the TranslationList. That way you separate the loading display away from your TranslationList as being it's concern. This also allows you to use the HOC in other areas.
The Translator can act as "container" component which does the data fetching/passing.
For example:
// The Loadable HOC
function Loadable(WrappedComponent) {
return function LoadableComponent({ loaded, ...otherProps }) {
return loaded
? <WrappedComponent {...otherProps} />
: <div>Loading...</div>
}
}
// Translation list doesn't need to know about "loaded" prop
function TranslationList({ translations }) {
return (
<ul>
{
translations.map((translation, index) =>
<li key={index}>{translation}</li>
)
}
</ul>
)
}
// We create our new composed component here.
const LoadableTranslationList = Loadable(TranslationList)
class Translator extends React.Component {
state = {
loaded: false,
translations: []
}
componentDidMount() {
// Let's simulate a data fetch, typically you are going to access
// a prop like this.props.textToTranslate and then pass that to
// an API or redux action to fetch the respective translations.
setTimeout(() => {
this.setState({
loaded: true,
translations: [ 'Bonjour', 'Goddag', 'Hola' ]
});
}, 2000);
}
render() {
const { loaded, translations } = this.state;
return (
<div>
<h3>Translations for "{this.props.textToTranslate}"</h3>
<LoadableTranslationList loaded={loaded} translations={translations} />
</div>
)
}
}
ReactDOM.render(<Translate textToTranslate="Hello" />)
Running example here: http://www.webpackbin.com/NyQnWe54W

Calling a function when ng-repeat has finished

What I am trying to implement is basically a "on ng repeat finished rendering" handler. I am able to detect when it is done but I can't figure out how to trigger a function from it.
Check the fiddle:http://jsfiddle.net/paulocoelho/BsMqq/3/
JS
var module = angular.module('testApp', [])
.directive('onFinishRender', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
element.ready(function () {
console.log("calling:"+attr.onFinishRender);
// CALL TEST HERE!
});
}
}
}
});
function myC($scope) {
$scope.ta = [1, 2, 3, 4, 5, 6];
function test() {
console.log("test executed");
}
}
HTML
<div ng-app="testApp" ng-controller="myC">
<p ng-repeat="t in ta" on-finish-render="test()">{{t}}</p>
</div>
Answer:
Working fiddle from finishingmove: http://jsfiddle.net/paulocoelho/BsMqq/4/
var module = angular.module('testApp', [])
.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit(attr.onFinishRender);
});
}
}
}
});
Notice that I didn't use .ready() but rather wrapped it in a $timeout. $timeout makes sure it's executed when the ng-repeated elements have REALLY finished rendering (because the $timeout will execute at the end of the current digest cycle -- and it will also call $apply internally, unlike setTimeout). So after the ng-repeat has finished, we use $emit to emit an event to outer scopes (sibling and parent scopes).
And then in your controller, you can catch it with $on:
$scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
//you also get the actual event object
//do stuff, execute functions -- whatever...
});
With html that looks something like this:
<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
<div>{{item.name}}}<div>
</div>
Use $evalAsync if you want your callback (i.e., test()) to be executed after the DOM is constructed, but before the browser renders. This will prevent flicker -- ref.
if (scope.$last) {
scope.$evalAsync(attr.onFinishRender);
}
Fiddle.
If you really want to call your callback after rendering, use $timeout:
if (scope.$last) {
$timeout(function() {
scope.$eval(attr.onFinishRender);
});
}
I prefer $eval instead of an event. With an event, we need to know the name of the event and add code to our controller for that event. With $eval, there is less coupling between the controller and the directive.
The answers that have been given so far will only work the first time that the ng-repeat gets rendered, but if you have a dynamic ng-repeat, meaning that you are going to be adding/deleting/filtering items, and you need to be notified every time that the ng-repeat gets rendered, those solutions won't work for you.
So, if you need to be notified EVERY TIME that the ng-repeat gets re-rendered and not just the first time, I've found a way to do that, it's quite 'hacky', but it will work fine if you know what you are doing. Use this $filter in your ng-repeat before you use any other $filter:
.filter('ngRepeatFinish', function($timeout){
return function(data){
var me = this;
var flagProperty = '__finishedRendering__';
if(!data[flagProperty]){
Object.defineProperty(
data,
flagProperty,
{enumerable:false, configurable:true, writable: false, value:{}});
$timeout(function(){
delete data[flagProperty];
me.$emit('ngRepeatFinished');
},0,false);
}
return data;
};
})
This will $emit an event called ngRepeatFinished every time that the ng-repeat gets rendered.
How to use it:
<li ng-repeat="item in (items|ngRepeatFinish) | filter:{name:namedFiltered}" >
The ngRepeatFinish filter needs to be applied directly to an Array or an Object defined in your $scope, you can apply other filters after.
How NOT to use it:
<li ng-repeat="item in (items | filter:{name:namedFiltered}) | ngRepeatFinish" >
Do not apply other filters first and then apply the ngRepeatFinish filter.
When should I use this?
If you want to apply certain css styles into the DOM after the list has finished rendering, because you need to have into account the new dimensions of the DOM elements that have been re-rendered by the ng-repeat. (BTW: those kind of operations should be done inside a directive)
What NOT TO DO in the function that handles the ngRepeatFinished event:
Do not perform a $scope.$apply in that function or you will put Angular in an endless loop that Angular won't be able to detect.
Do not use it for making changes in the $scope properties, because those changes won't be reflected in your view until the next $digest loop, and since you can't perform an $scope.$apply they won't be of any use.
"But filters are not meant to be used like that!!"
No, they are not, this is a hack, if you don't like it don't use it. If you know a better way to accomplish the same thing please let me know it.
Summarizing
This is a hack, and using it in the wrong way is dangerous, use it only for applying styles after the ng-repeat has finished rendering and you shouldn't have any issues.
If you need to call different functions for different ng-repeats on the same controller you can try something like this:
The directive:
var module = angular.module('testApp', [])
.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit(attr.broadcasteventname ? attr.broadcasteventname : 'ngRepeatFinished');
});
}
}
}
});
In your controller, catch events with $on:
$scope.$on('ngRepeatBroadcast1', function(ngRepeatFinishedEvent) {
// Do something
});
$scope.$on('ngRepeatBroadcast2', function(ngRepeatFinishedEvent) {
// Do something
});
In your template with multiple ng-repeat
<div ng-repeat="item in collection1" on-finish-render broadcasteventname="ngRepeatBroadcast1">
<div>{{item.name}}}<div>
</div>
<div ng-repeat="item in collection2" on-finish-render broadcasteventname="ngRepeatBroadcast2">
<div>{{item.name}}}<div>
</div>
The other solutions will work fine on initial page load, but calling $timeout from the controller is the only way to ensure that your function is called when the model changes. Here is a working fiddle that uses $timeout. For your example it would be:
.controller('myC', function ($scope, $timeout) {
$scope.$watch("ta", function (newValue, oldValue) {
$timeout(function () {
test();
});
});
ngRepeat will only evaluate a directive when the row content is new, so if you remove items from your list, onFinishRender will not fire. For example, try entering filter values in these fiddles emit.
If you’re not averse to using double-dollar scope props and you’re writing a directive whose only content is a repeat, there is a pretty simple solution (assuming you only care about the initial render). In the link function:
const dereg = scope.$watch('$$childTail.$last', last => {
if (last) {
dereg();
// do yr stuff -- you may still need a $timeout here
}
});
This is useful for cases where you have a directive that needs to do DOM manip based on the widths or heights of the members of a rendered list (which I think is the most likely reason one would ask this question), but it’s not as generic as the other solutions that have been proposed.
I'm very surprised not to see the most simple solution among the answers to this question.
What you want to do is add an ngInit directive on your repeated element (the element with the ngRepeat directive) checking for $last (a special variable set in scope by ngRepeat which indicates that the repeated element is the last in the list). If $last is true, we're rendering the last element and we can call the function we want.
ng-init="$last && test()"
The complete code for your HTML markup would be:
<div ng-app="testApp" ng-controller="myC">
<p ng-repeat="t in ta" ng-init="$last && test()">{{t}}</p>
</div>
You don't need any extra JS code in your app besides the scope function you want to call (in this case, test) since ngInit is provided by Angular.js. Just make sure to have your test function in the scope so that it can be accessed from the template:
$scope.test = function test() {
console.log("test executed");
}
A solution for this problem with a filtered ngRepeat could have been with Mutation events, but they are deprecated (without immediate replacement).
Then I thought of another easy one:
app.directive('filtered',function($timeout) {
return {
restrict: 'A',link: function (scope,element,attr) {
var elm = element[0]
,nodePrototype = Node.prototype
,timeout
,slice = Array.prototype.slice
;
elm.insertBefore = alt.bind(null,nodePrototype.insertBefore);
elm.removeChild = alt.bind(null,nodePrototype.removeChild);
function alt(fn){
fn.apply(elm,slice.call(arguments,1));
timeout&&$timeout.cancel(timeout);
timeout = $timeout(altDone);
}
function altDone(){
timeout = null;
console.log('Filtered! ...fire an event or something');
}
}
};
});
This hooks into the Node.prototype methods of the parent element with a one-tick $timeout to watch for successive modifications.
It works mostly correct but I did get some cases where the altDone would be called twice.
Again... add this directive to the parent of the ngRepeat.
Very easy, this is how I did it.
.directive('blockOnRender', function ($blockUI) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
if (scope.$first) {
$blockUI.blockElement($(element).parent());
}
if (scope.$last) {
$blockUI.unblockElement($(element).parent());
}
}
};
})
Please have a look at the fiddle, http://jsfiddle.net/yNXS2/. Since the directive you created didn't created a new scope i continued in the way.
$scope.test = function(){... made that happen.

Resources